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.
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:
Both must stay in sync. A beautiful drag-and-drop UI backed by an API that cannot handle reordering is a design failure.
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 |
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.
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.
The UI/UX design phase turns wireframes into pixel-perfect mockups. Build a design system — a shared vocabulary of reusable components:
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.
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:
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.
“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.