Designing and Prototyping

In the previous post, we turned our task management app idea into wireframes. Now it is time to turn those wireframes into something real. This is where you define how the system works, not just how it looks.

1. From Wireframes to Design

Wireframes show structure — where elements go on the page. Design answers everything else: visual identity, interaction patterns, error states, and the system architecture underneath.

For our task app, the wireframes showed boards, columns, and draggable cards. Now we answer the hard questions: How does the frontend talk to the backend? What does the data look like? What happens when two users drag the same card?

Design runs on two parallel tracks:

  • Technical design — architecture, APIs, database schema
  • Visual design — UI components, interactions, responsive behavior

Both must stay in sync. A beautiful drag-and-drop UI backed by an API that cannot handle reordering is a design failure.

2. System Architecture Design

Before writing code, define the high-level architecture. Our task app uses a classic client-server pattern with a REST API layer:

Frontend (React SPA)  --HTTPS/JSON-->  API Gateway (Nginx)  --HTTP-->  Backend (REST API)  --SQL-->  Database (PostgreSQL)
Component Responsibility Technology
Frontend UI rendering, user interactions, local state React, TypeScript
API Gateway Routing, rate limiting, SSL, authentication Nginx or AWS ALB
Backend Business logic, validation, authorization Spring Boot or Flask
Database Persistent storage, data integrity PostgreSQL

3. API Design

The API contract is the agreement between frontend and backend. Design it before implementation so both teams can work in parallel.

Method Endpoint Description
GET /api/v1/boards List all boards for the authenticated user
POST /api/v1/boards Create a new board
GET /api/v1/boards/{id}/tasks List all tasks on a board
POST /api/v1/tasks Create a new task
PUT /api/v1/tasks/{id} Update a task
DELETE /api/v1/tasks/{id} Delete a task
GET /api/v1/users/me Get current user profile

Here is the Spring Boot controller contract defining these endpoints:

// TaskController.java — API contract for task management

@RestController
@RequestMapping("/api/v1")
public class TaskController {

    public record TaskRequest(
        String title, String description,
        String status, Long columnId, Long assigneeId
    ) {}

    public record TaskResponse(
        Long id, String title, String description,
        String status, String columnName, String assigneeName,
        LocalDateTime createdAt, LocalDateTime updatedAt
    ) {}

    @GetMapping("/boards/{boardId}/tasks")
    public ResponseEntity<List<TaskResponse>> getTasksByBoard(
            @PathVariable Long boardId) {
        return ResponseEntity.ok(taskService.findByBoard(boardId));
    }

    @PostMapping("/tasks")
    public ResponseEntity<TaskResponse> createTask(
            @RequestBody @Valid TaskRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(taskService.create(request));
    }

    @PutMapping("/tasks/{id}")
    public ResponseEntity<TaskResponse> updateTask(
            @PathVariable Long id,
            @RequestBody @Valid TaskRequest request) {
        return ResponseEntity.ok(taskService.update(id, request));
    }

    @DeleteMapping("/tasks/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
        taskService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

The same contract in Flask with Python dataclasses:

# task_controller.py — API contract for task management

from dataclasses import dataclass, asdict
from datetime import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)

@dataclass
class TaskRequest:
    title: str
    description: str
    status: str
    column_id: int
    assignee_id: int

@dataclass
class TaskResponse:
    id: int
    title: str
    description: str
    status: str
    column_name: str
    assignee_name: str
    created_at: datetime
    updated_at: datetime

@app.route("/api/v1/boards/<int:board_id>/tasks", methods=["GET"])
def get_tasks_by_board(board_id):
    tasks = task_service.find_by_board(board_id)
    return jsonify([asdict(t) for t in tasks]), 200

@app.route("/api/v1/tasks", methods=["POST"])
def create_task():
    data = TaskRequest(**request.get_json())
    return jsonify(asdict(task_service.create(data))), 201

@app.route("/api/v1/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
    data = TaskRequest(**request.get_json())
    return jsonify(asdict(task_service.update(task_id, data))), 200

@app.route("/api/v1/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
    task_service.delete(task_id)
    return "", 204

Both implementations define the same contract. The DTOs enforce data shape. The HTTP methods and status codes follow REST conventions. This is the specification both teams work against.

4. Database Design

The database schema is the foundation everything rests on. Here is the schema for our task app:

Table Column Type Constraint
users id, email, display_name, created_at BIGINT, VARCHAR, VARCHAR, TIMESTAMP PK, UNIQUE NOT NULL, NOT NULL, DEFAULT NOW()
boards id, name, owner_id, created_at BIGINT, VARCHAR, BIGINT, TIMESTAMP PK, NOT NULL, FK -> users(id), DEFAULT NOW()
columns id, name, board_id, position BIGINT, VARCHAR, BIGINT, INTEGER PK, NOT NULL, FK -> boards(id), NOT NULL
tasks id, title, description, column_id, assignee_id, position, created_at, updated_at BIGINT, VARCHAR, TEXT, BIGINT, BIGINT, INTEGER, TIMESTAMP, TIMESTAMP PK, NOT NULL, NULLABLE, FK -> columns(id), FK -> users(id), NOT NULL, DEFAULT NOW(), DEFAULT NOW()

Key relationships: A user owns many boards. A board has many columns. A column has many tasks. A task belongs to one column and one assignee. The position field enables drag-and-drop reordering — move a card and you update the position integers.

5. UI/UX Design

The UI/UX design phase turns wireframes into pixel-perfect mockups. Build a design system — a shared vocabulary of reusable components:

  • Colors — Primary blue (#2563EB) for actions, grays for backgrounds, red for destructive actions
  • Typography — Inter for UI text, monospace for IDs
  • Components — Task cards, column headers, board nav, modals, dropdowns
  • Spacing — 4px base unit for consistent padding and margins

Tools like Figma and Adobe XD let designers and developers collaborate on the same file, inspect spacing, and export assets.

Critical principle: design for states, not just screens. Every component needs empty, loading, error, and populated state designs.

6. Prototyping

A prototype is a clickable, interactive mockup that stakeholders can experience without production code. Click “Create Task” and a modal opens. Drag a card between columns. Navigate between boards.

Prototyping serves two purposes:

  1. Validation — Put it in front of real users. Where do they hesitate? Fix those problems now, not after 10,000 lines of code.
  2. Alignment — Walk stakeholders through the prototype. Misunderstandings surface early, not during code review.

Figma’s prototyping mode lets you define click targets, transitions, and flows directly on design frames. Remember: a prototype is not production code. It is a communication tool.

7. Senior Tip

“Design is not how it looks, it’s how it works.” — Steve Jobs

Every hour spent on design saves five hours of rework during implementation. Focus on user experience over aesthetics. A plain-looking app with clear navigation and predictable behavior beats a stunning app that confuses users. Get the API contracts right. Get the data model right. The colors are the easy part.

In the next post, we start building. That is where implementation begins.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *