Building a complete CRUD REST API end-to-end — project structure, error handling, dotenv, nodemon, and testing with curl and Postman.
Module F-8 — Your First Real Application
What this module covers: Modules F-1 through F-7 gave you all the individual pieces. This module assembles them into a real, structured project. We build a Blog API — full CRUD for posts and users, proper project structure, environment configuration, error handling, and a working Prisma database layer. By the end you will have a template that reflects how real Node.js applications are organised, and you will understand why each layer exists.
What We Are Building
A Blog API with two resources:
- Users — register, read profile
- Posts — create, read, update, delete blog posts
Endpoints:
This is deliberately simple — no authentication yet (that's P-2). The goal is clean project structure and solid fundamentals.
Project Structure
This is the layered architecture we will formalise in P-1. For now the pattern is: routes handle HTTP, the db/ layer handles data access, and middleware/ holds cross-cutting concerns.
Step 1: Project Initialisation
Update package.json:
Create .gitignore:
Create .env:
Create .env.example (committed — shows what vars are needed without exposing values):
Step 2: Database Schema
Run the migration:
This creates the tables in your PostgreSQL database and generates the Prisma Client.
Step 3: Prisma Client Singleton
One PrismaClient instance for the entire application. Creating one per request wastes connections and causes pool exhaustion.
Step 4: Route Handlers
Step 5: Error Handler Middleware
Step 6: Application Entry Point
Step 7: nodemon for Development
Nodemon watches src/ and restarts the server whenever you save a file.
Testing the API
What This Structure Gives You
Looking at the project layout again:
Why separate db/ from routes/?
Your route handlers are now thin — they validate input, call a function, and send a response. All database logic is in db/. If you switch from Prisma to raw SQL tomorrow, you change db/ only. Routes stay the same.
Why a dedicated error handler? All errors flow to one place. You change error formatting once. You add logging once. Every route benefits automatically.
Why dotenv/config at the top of index.js?
Environment variables must be loaded before anything else reads them. Loading it first in the entry file ensures process.env.DATABASE_URL is available when prisma.js is imported.
What Comes Next
This application is the baseline. As you move into the Practitioner phase, you will add:
- P-1: Proper layered architecture (service layer between routes and DB)
- P-2: JWT authentication — protect routes, know who is making requests
- P-3: TypeScript — full type safety from route parameters to database results
- P-4: Zod validation — replace manual
if (!title)checks with schema declarations - P-5: Tests — Jest and Supertest to verify every endpoint automatically
- P-8: Docker — containerise the entire app and database
Each addition builds on the structure you have here. The fundamentals do not change — they get reinforced.
Summary
- A real application has layers: routes (HTTP), services (logic), and data (database). Even this small app separates HTTP handling from database access.
dotenv/configloads environment variables first, before any imports that need them.- One Prisma client instance per application. Never create one per request.
- Pass errors to
next(err)in every route handler. The global error handler catches them all. - A health check endpoint (
GET /health) is a small addition that makes your app ready for Docker and Kubernetes load balancer checks. .env.exampleis committed;.envis not. Everyone on the team knows what configuration the app needs.
Next: Event Emitters — the mechanism underlying everything in Node.js from HTTP servers to file streams.
Why is it important to create a single PrismaClient instance and export it as a singleton rather than instantiating it inside route handlers?
In the context of the blog API's error handling, why do the route handlers catch errors and pass them to next(err)?
Why is .env.example committed to version control while .env is added to .gitignore?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion