Time to build. Express is the most widely used Node.js web framework: minimal, unopinionated, and backed by fifteen years of ecosystem. In this lesson we set up a project from scratch and implement a books API with proper structure, middleware, and error handling, the skeleton you can reuse for any service.
With Node.js 18+ installed:
mkdir books-api && cd books-api
npm init -y
npm install express
npm install --save-dev nodemonAdd scripts to package.json so npm run dev restarts the server on every file change:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}Everything in Express is middleware: functions with the signature (req, res, next) that run in order for each request. Each one can inspect or modify the request, end the response, or call next() to pass control along. Routes are just middleware that usually end the chain.
app.use(express.json()); // parse JSON bodies onto req.body
app.use((req, res, next) => { // tiny request logger
console.log(new Date().toISOString(), req.method, req.url);
next();
});This pipeline model is why Express stays small: logging, auth, CORS, and rate limiting are all just middleware you compose in order. Order matters; a logger registered after your routes never sees them.
Route paths can contain named parameters, exposed on req.params. Query strings appear on req.query and parsed bodies on req.body.
app.get('/api/books/:id', (req, res) => {
const book = db.find(b => b.id === Number(req.params.id));
if (!book) return res.status(404).json({ error: 'book not found' });
res.json(book);
});As an app grows, group related routes with express.Router into their own files (routes/books.js) and mount them with app.use('/api/books', booksRouter). This keeps each file focused on one resource, mirroring the resource-oriented URL design from earlier lessons.
Express recognizes error-handling middleware by its four-argument signature. Register one after all routes, and route handlers can simply throw or call next(err) instead of formatting errors in twenty places.
app.use((err, req, res, next) => {
console.error(err);
const status = err.status || 500;
res.status(status).json({
error: { code: err.code || 'internal_error', message: status === 500 ? 'Something went wrong' : err.message }
});
});Note the 500 branch hides internal details from callers while logging the full error server-side, a security habit we will revisit in the security lesson.
Our sample keeps data in an array so it runs anywhere, but the structure is production-shaped: a server.js entry point, routers per resource, middleware for cross-cutting concerns, and one error handler. From here, real projects typically add a database layer (Prisma, Mongoose, or a query builder), a validation library like Zod on every request body, environment-based config via process.env, and helmet plus cors middleware. Each drops into the same skeleton without restructuring.
Start with npm run dev, then exercise it exactly as the previous lesson taught: curl the happy path, send invalid bodies to confirm 400s, request missing IDs to confirm 404s. If you saved a Postman collection for the CRUD design lesson, point its {{baseUrl}} at http://localhost:3000 and run the whole lifecycle.
Next up: API Authentication with API Keys, our first layer of access control.