Building APIs with Express
A complete small Express app: routes, middleware, route params, JSON bodies, and error handling.
Why Express
Express is the most widely used web framework for Node.js — a thin, unopinionated layer over Node's built-in http module that adds routing, middleware, and conveniences for building APIs without dictating how the rest of your application is structured.
npm install express
A minimal server
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
app.listen(3000, () => {
console.log('Server listening on http://localhost:3000');
});
Routes and route params
Routes match an HTTP method and a URL path. A path segment prefixed with : captures a route parameter, available on req.params:
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
curl http://localhost:3000/users/42
# {"id":"42"}
Parsing JSON request bodies
Express doesn't parse request bodies by default — the built-in express.json() middleware handles it, populating req.body:
app.use(express.json());
app.post('/users', (req, res) => {
const { name, email } = req.body;
res.status(201).json({ id: 101, name, email });
});
Middleware
Middleware is a function that runs during the request/response cycle, with access to the request, the response, and a next function to pass control to the next middleware (or the final route handler) in the chain:
function requestLogger(req, res, next) {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next(); // without this, the request hangs forever — it never reaches the next handler
}
app.use(requestLogger);
app.use() registers middleware that runs on every request, in the order it was registered. Middleware can also be scoped to a specific route:
function requireAuth(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Missing authorization header' });
}
next();
}
app.get('/admin/stats', requireAuth, (req, res) => {
res.json({ activeUsers: 128 });
});
Error-handling middleware
Express recognizes an error-handling middleware by its four parameters (err, req, res, next — the extra leading err argument is what distinguishes it from ordinary middleware). It should be registered last, after all other routes and middleware:
app.get('/risky', (req, res, next) => {
try {
throw new Error('Something went wrong');
} catch (error) {
next(error); // passes the error straight to the error-handling middleware
}
});
// Error-handling middleware — must come after all other app.use()/routes
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
A complete small app
Putting the pieces together — a small JSON API with routes, custom logging middleware, an auth-gated route, JSON body parsing, and centralized error handling:
import express from 'express';
const app = express();
app.use(express.json());
// Runs on every request, before any route
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
const users = [
{ id: 1, name: 'Ali Raza' },
{ id: 2, name: 'Zara Khan' },
];
app.get('/users', (req, res) => {
res.json(users);
});
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === Number(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.post('/users', (req, res) => {
const { name } = req.body;
if (!name) {
return res.status(400).json({ error: 'name is required' });
}
const newUser = { id: users.length + 1, name };
users.push(newUser);
res.status(201).json(newUser);
});
// 404 fallback for unmatched routes
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Error-handling middleware — always last
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, () => console.log('Listening on port 3000'));
Common mistakes
- Forgetting to call
next()inside a middleware function — the request simply hangs with no response and no error, since Express has no way to know the middleware is "done." - Registering error-handling middleware (the four-argument kind) before other routes instead of after — Express matches middleware in registration order, so an error handler registered too early never sees errors thrown by routes defined after it.
- Forgetting
express.json()and then being confused whyreq.bodyisundefinedon everyPOSTrequest.
Interview questions
Q: What is middleware in Express, and what does calling next() actually do?
Middleware is a function with access to the request, response, and a next callback, run in the order it's registered before a request reaches its final route handler. Calling next() passes control forward to the next middleware (or route handler) in the chain; if a middleware never calls it (and doesn't send a response itself), the request hangs indefinitely with no response ever sent.
Q: How does Express know a middleware function is meant to handle errors?
By its signature — an error-handling middleware takes four parameters (err, req, res, next) instead of the usual three. It must also be registered after all normal routes and middleware, since Express only reaches it when something earlier calls next(err) with an argument, or throws inside an async route wrapped to forward its error.
Q: Why does the order routes and middleware are registered in matter in Express?
Because Express processes middleware and routes in the exact order they were registered with app.use()/app.get()/etc., checking each one in sequence until it finds a match (or an error-handler, if an error occurred). Registering a logging middleware after the routes it's meant to log, or an auth check after the route it's meant to protect, means it simply never runs for those requests.