Warming up the neural circuits...
By the end of this project you will have shipped:
/notes and /todosReading 10 chapters teaches you the words. Building one project teaches you the language. This is the bar for "I am a backend engineer" — not a senior one yet, but a real one.
This project applies every L1 chapter:
You should be able to finish this in 4–8 hours of focused work.
Two resources:
/notes (we built a stub in chapter 6)/todosPOST /api/v1/notes 201 + Location
GET /api/v1/notes 200 (paginated, filterable by ?tag, ?q)
GET /api/v1/notes/:id 200 / 404
PATCH /api/v1/notes/:id 200 / 404 / 422
DELETE /api/v1/notes/:id 204 / 404
POST /api/v1/todos 201 + Location
GET /api/v1/todos 200 (paginated, filterable by ?done, ?noteId)
GET /api/v1/todos/:id 200 / 404
PATCH /api/v1/todos/:id 200 / 404 / 422
DELETE /api/v1/todos/:id 204 / 404X-Request-Id (echoed in response).issues.{ error, requestId } — no .GET /health returns { status: 'ok' }.notes-todo-api/
├── prisma/
│ └── schema.prisma
├── src/
│ ├── server.ts # bootstrap (express + listen)
│ ├── app.ts # express setup + middleware
│ ├── config.ts # env vars (validated)
│ ├── lib/
│ │ ├── prisma.ts # PrismaClient singleton
│ │ ├── logger.ts # pino logger
│ │ └── error.ts # ApiError class
│ ├── middleware/
│ │ ├── requestId.ts
│ │ ├── validate.ts
│ │ ├── asyncHandler.ts
│ │ └── errorHandler.ts
│ ├── modules/
│ │ ├── notes/
│ │ │ ├── notes.routes.ts
│ │ │ ├── notes.service.ts
│ │ │ ├── notes.schemas.ts
│ │ │ └── notes.test.ts
│ │ └── todos/
│ │ ├── todos.routes.ts
│ │ ├── todos.service.ts
│ │ ├── todos.schemas.ts
│ │ └── todos.test.ts
├── .env.example
├── package.json
├── tsconfig.json
└── README.mdThree-layer per resource: routes → service → DB. The route validates, the service holds business logic, the DB layer (Prisma) reads/writes.
mkdir notes-todo-api && cd notes-todo-api
npm init -y
npm install express zod pino pino-http prisma @prisma/client dotenv
npm install -D typescript tsx @types/express @types/node vitest supertest @types/supertest
npx tsc --init
npx prisma init --datasource-provider postgresqlSet up package.json scripts:
{
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "vitest",
"migrate:dev": "prisma migrate dev
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: notes_todo
ports: ["5432:5432"]
volumes: [pgdata:/var/lib/postgresql/data]
volumes: { pgdata: {} }docker compose up -dgenerator client { provider = "prisma-client-js" }
datasource db { provider = "postgresql"; url = env("DATABASE_URL") }
model Note {
id String @id @default(uuid())
title String
body String @default("")
tags String
echo 'DATABASE_URL="postgresql://postgres:secret@localhost:5432/notes_todo"' > .env
npm run migrate:dev -- --name initimport express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import pinoHttp from 'pino-http';
import { logger } from './lib/logger';
import
import { buildApp } from './app';
import { logger } from './lib/logger';
const PORT = process.env.PORT || 3000;
const app = buildApp();
const
import { z } from 'zod';
export const createNote = z.object({
title: z.string().min(1).max(200),
body: z.
import { prisma } from '../../lib/prisma';
import { ApiError } from '../../lib/error';
export const notesService = {
async create(data) { return prisma.note.create({
import { Router } from 'express';
import { validate } from '../../middleware/validate';
import { asyncHandler } from '../../middleware/asyncHandler';
import { createNote, updateNote, listNotesQuery, idParam
Same shape, swap fields. Add done filter:
export const listTodosQuery = z.object({
done: z.enum(['true','false']).transform(v => v === 'true').optional(),
noteId: z
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { buildApp } from '../../app';
const app = buildApp();
describe
Run: npm test.
npm run dev
# Create
curl -X POST http://localhost:3000/api/v1/notes \
-H "Content-Type: application/json" \
-d '{"title":"Buy milk","tags":["grocery"]}'
# List
curl http://localhost:3000/api/v1/notes.http file with example requests.?tag=... filter using Prisma's tags: { has: tag }.GET /api/v1/notes/:id/todos endpoint that returns linked todos.deletedAt).zod-to-openapi.include in Prisma.await services.A small but real backend. Postgres-backed. Validated. Logged. Tested. Containerized.
This is what every API at every SaaS looks like under the hood. The next levels (L2–L7) just expand on this template: more sophistication in DB design, security, scale, deployment, and AI.
If this clicked — you're a backend engineer. Welcome.
A two-resource, Postgres-backed Express API with validation, error handling, structured logging, and tests. The same shape powers nearly every SaaS product. Once you've built it, every concept in L2–L7 has somewhere familiar to land.