Project: Production REST API

0/3 in this phase0/48 across the roadmap

📖 Concept

Build a complete, production-ready REST API from scratch, applying everything learned across all previous phases. This project integrates Express.js, databases, authentication, testing, Docker, and CI/CD into a single cohesive application.

Project: Task Management API (like Trello/Jira)

Features:

  • User registration and JWT authentication
  • Workspaces with role-based access (owner, admin, member)
  • Boards with lists and cards (Kanban style)
  • Card assignments, labels, due dates, comments
  • File attachments (upload to S3)
  • Activity log (who did what when)
  • Real-time updates via WebSockets
  • Full-text search across cards and comments

Tech stack:

Backend:   Express.js + TypeScript
Database:  PostgreSQL (Prisma ORM)
Cache:     Redis (sessions, cache, rate limiting)
Auth:      JWT + refresh tokens
Files:     AWS S3 / MinIO
Queue:     BullMQ (email notifications, file processing)
Testing:   Jest + Supertest
Deploy:    Docker + GitHub Actions CI/CD
Monitor:   Prometheus + Grafana

Architecture:

Client → nginx → Express API → PostgreSQL
                      ↕               ↕
                    Redis          BullMQ Workers
                      ↕
                   Socket.IO

This project tests your ability to:

  • Design a clean, maintainable project structure
  • Implement complex business logic with proper error handling
  • Write comprehensive tests (unit + integration)
  • Deploy with Docker and CI/CD
  • Monitor and debug in production

🏠 Real-world analogy: This is the capstone project — like building a fully furnished house. You've learned about foundations (Node.js core), framing (Express), plumbing (databases), electrical (auth), and painting (frontend). Now you put it all together into a house someone can actually live in.

💻 Code Example

codeTap to expand ⛶
1// Production REST API — Project Structure & Core Implementation
2
3// === Project structure ===
4// src/
5// ├── config/ → Environment, database, Redis config
6// ├── middleware/ → Auth, validation, error handling, rate limiting
7// ├── modules/ → Feature modules (users, boards, cards)
8// │ ├── users/
9// │ │ ├── user.controller.js
10// │ │ ├── user.service.js
11// │ │ ├── user.repository.js
12// │ │ ├── user.routes.js
13// │ │ └── user.validation.js
14// │ ├── boards/
15// │ └── cards/
16// ├── shared/ → Shared utilities, base classes
17// ├── jobs/ → BullMQ job processors
18// ├── app.js → Express app setup
19// └── server.js → Entry point (app.listen)
20// tests/
21// ├── unit/
22// ├── integration/
23// └── fixtures/
24// prisma/
25// ├── schema.prisma
26// └── migrations/
27// docker-compose.yml
28// Dockerfile
29// .github/workflows/ci.yml
30
31// === src/app.js — Application Setup ===
32const express = require("express");
33const helmet = require("helmet");
34const cors = require("cors");
35const rateLimit = require("express-rate-limit");
36
37function createApp(dependencies) {
38 const app = express();
39
40 // Security
41 app.use(helmet());
42 app.use(cors({ origin: process.env.CORS_ORIGIN, credentials: true }));
43 app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
44 app.use(express.json({ limit: "10mb" }));
45
46 // Request ID
47 app.use((req, res, next) => {
48 req.requestId = req.headers["x-request-id"] || require("crypto").randomUUID();
49 res.setHeader("X-Request-Id", req.requestId);
50 next();
51 });
52
53 // Health check
54 app.get("/health", async (req, res) => {
55 res.json({
56 status: "healthy",
57 version: process.env.npm_package_version,
58 uptime: process.uptime(),
59 });
60 });
61
62 // API routes
63 app.use("/api/v1/auth", dependencies.authRoutes);
64 app.use("/api/v1/users", dependencies.userRoutes);
65 app.use("/api/v1/boards", dependencies.boardRoutes);
66 app.use("/api/v1/cards", dependencies.cardRoutes);
67
68 // 404
69 app.use((req, res) => {
70 res.status(404).json({ error: `${req.method} ${req.path} not found` });
71 });
72
73 // Error handler
74 app.use((err, req, res, next) => {
75 const statusCode = err.statusCode || 500;
76 const message = err.isOperational ? err.message : "Internal server error";
77
78 console.error({
79 requestId: req.requestId,
80 error: err.message,
81 stack: err.stack,
82 path: req.path,
83 });
84
85 res.status(statusCode).json({
86 success: false,
87 error: message,
88 requestId: req.requestId,
89 });
90 });
91
92 return app;
93}
94
95// === src/modules/cards/card.service.js ===
96class CardService {
97 constructor(cardRepo, boardService, notificationQueue, cache) {
98 this.cardRepo = cardRepo;
99 this.boardService = boardService;
100 this.notificationQueue = notificationQueue;
101 this.cache = cache;
102 }
103
104 async createCard(boardId, data, userId) {
105 // Verify board access
106 const board = await this.boardService.getBoard(boardId, userId);
107 if (!board) throw new AppError("Board not found", 404);
108
109 const card = await this.cardRepo.create({
110 ...data,
111 boardId,
112 creatorId: userId,
113 position: await this.cardRepo.getNextPosition(boardId, data.listId),
114 });
115
116 // Invalidate cache
117 await this.cache.del(`board:${boardId}:cards`);
118
119 // Notify assigned users
120 if (data.assigneeIds?.length) {
121 await this.notificationQueue.add("card-assigned", {
122 cardId: card.id,
123 assigneeIds: data.assigneeIds,
124 assignedBy: userId,
125 });
126 }
127
128 return card;
129 }
130
131 async moveCard(cardId, targetListId, position, userId) {
132 const card = await this.cardRepo.findById(cardId);
133 if (!card) throw new AppError("Card not found", 404);
134
135 await this.boardService.verifyAccess(card.boardId, userId);
136
137 const updated = await this.cardRepo.transaction(async (tx) => {
138 // Reorder cards in source and target lists
139 await this.cardRepo.reorderAfterRemove(card.listId, card.position, tx);
140 await this.cardRepo.reorderAfterInsert(targetListId, position, tx);
141
142 return this.cardRepo.update(cardId, {
143 listId: targetListId,
144 position,
145 }, tx);
146 });
147
148 await this.cache.del(`board:${card.boardId}:cards`);
149 return updated;
150 }
151
152 async searchCards(query, userId) {
153 const boards = await this.boardService.getUserBoards(userId);
154 const boardIds = boards.map((b) => b.id);
155
156 return this.cardRepo.fullTextSearch(query, boardIds);
157 }
158}
159
160class AppError extends Error {
161 constructor(message, statusCode) {
162 super(message);
163 this.statusCode = statusCode;
164 this.isOperational = true;
165 }
166}
167
168module.exports = { createApp, CardService, AppError };

🏋️ Practice Exercise

Exercises:

  1. Build the complete Task Management API with users, boards, lists, and cards
  2. Implement role-based access: workspace owners can manage members; members can only view/edit assigned cards
  3. Add real-time updates — when a card is moved, all connected users see the change instantly
  4. Implement full-text search with PostgreSQL tsvector or Elasticsearch
  5. Write integration tests for the complete API — achieve 80%+ code coverage
  6. Dockerize the application and deploy with CI/CD — include database migrations in the pipeline

⚠️ Common Mistakes

  • Not designing the project structure upfront — jumping into code without architecture leads to spaghetti code; plan modules and layers first

  • Mixing concerns in route handlers — controllers should only handle HTTP; business logic belongs in services; data access in repositories

  • Skipping error handling for edge cases — real users will send unexpected data; test with invalid, missing, and malformed inputs

  • Not writing tests during development — retrofitting tests is harder and less effective; write tests alongside features

  • Deploying without monitoring — you can't fix what you can't measure; set up logging, metrics, and error tracking from day one

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Project: Production REST API