Answer:
Raw http module — everything is manual:
const http = require('http');
http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/users') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
} else {
res.writeHead(404);
res.end('Not found');
}
}).listen(3000);
You hand-roll routing, method checks, body parsing, headers, and content types.
Express — a thin framework over http:
const express = require('express');
const app = express();
app.use(express.json()); // body parsing
app.get('/users', (req, res) => res.json(users));
app.get('/users/:id', (req, res) => res.json(findUser(req.params.id)));
app.listen(3000);
What Express provides:
- Routing — match method + path patterns, with params (
/users/:id) and routers for modular structure. - Middleware pipeline — compose cross-cutting concerns (logging, auth, parsing, CORS).
- Request/response helpers —
req.params,req.query,req.body,res.json(),res.status(),res.redirect(). - Static files & views, and a huge middleware ecosystem (helmet, morgan, multer, ...).
Interview point: Express doesn't replace http — it wraps it, giving structure and convenience while still ultimately using the same server. Alternatives (Fastify, Koa, NestJS) make different trade-offs around performance, async design, and structure.
Answer: Middleware is the core abstraction in Express — a function that runs during the request/response cycle.
Signature and flow:
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next(); // pass control to the next middleware/route
}
app.use(logger);
Each middleware can:
- run code / modify
reqorres(e.g., attachreq.user), - end the cycle by sending a response, or
- call
next()to continue, ornext(err)to jump to error handling.
Execution order = registration order:
app.use(express.json()); // 1. parse JSON body
app.use(requestLogger); // 2. log
app.use('/api', authenticate); // 3. auth for /api routes
app.get('/api/me', handler); // 4. route handler
app.use(errorHandler); // 5. error middleware (last)
If express.json() were registered after the route, req.body would be undefined in the handler — a classic ordering bug.
Types of middleware:
- Application-level —
app.use(fn). - Router-level — mounted on an
express.Router(). - Path-scoped —
app.use('/admin', fn)runs only for matching paths. - Built-in —
express.json(),express.static(). - Third-party —
helmet,cors,morgan. - Error-handling — four args
(err, req, res, next).
Key idea: it's a pipeline — control flows top to bottom until something sends a response or calls next(err).
Answer: Express distinguishes error middleware from normal middleware by its arity: it takes four arguments.
// Registered AFTER all routes
app.use((err, req, res, next) => {
console.error(err.stack);
const status = err.statusCode || 500;
res.status(status).json({ error: err.message || 'Internal Server Error' });
});
How Express reaches it:
- You call
next(err)from any middleware/route. - A synchronous throw in a handler is caught by Express and routed here automatically.
- Async rejections: in Express 5, a rejected promise from an async handler is forwarded automatically. In Express 4, it is not — you must catch and
next(err)yourself, often via a wrapper:
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.getUser(req.params.id); // rejection → next(err)
res.json(user);
}));
Best practices:
- Keep a single central error handler; don't scatter response formatting.
- Log full details server-side, but return a safe, generic message to clients (don't leak stack traces or internals in production).
- Combine with custom error classes carrying
statusCodeso the handler maps errors to the right HTTP code. - Add a catch-all 404 handler before the error handler for unmatched routes.
Answer:
Basic routing — method + path → handler:
app.get('/users', listUsers);
app.post('/users', createUser);
app.put('/users/:id', updateUser);
app.delete('/users/:id', deleteUser);
Route parameters and query strings:
// GET /users/42?fields=name
app.get('/users/:id', (req, res) => {
req.params.id; // '42' (path parameter)
req.query.fields; // 'name' (query string)
});
Modular routers — group related routes and mount them:
// routes/users.js
const router = require('express').Router();
router.get('/', listUsers); // GET /users
router.get('/:id', getUser); // GET /users/:id
router.post('/', createUser); // POST /users
module.exports = router;
// app.js
app.use('/users', require('./routes/users'));
This keeps a growing app organized — one router file per resource — and lets you attach router-level middleware (e.g., auth for all /admin routes).
Useful details:
- Routes are matched in order; put more specific routes before catch-alls.
app.route('/users').get(...).post(...)chains methods for one path.- Patterns support optional segments and (in Express 4) regex; Express 5 changed some pattern syntax.
- A final
app.use((req, res) => res.status(404).json({error: 'Not found'}))handles unmatched routes.
Answer:
Resources as nouns, methods as verbs:
GET /users # list
POST /users # create
GET /users/:id # read one
PUT /users/:id # replace
PATCH /users/:id # partial update
DELETE /users/:id # delete
GET /users/:id/orders # nested/related resource
Avoid verbs in paths (/getUsers, /createUser) — the HTTP method conveys the action.
Meaningful status codes:
200 OK,201 Created,204 No Content400 Bad Request,401 Unauthorized,403 Forbidden,404 Not Found,409 Conflict,422 Unprocessable Entity500 Internal Server Error,503 Service Unavailable
Collections need pagination/filtering/sorting:
GET /users?page=2&limit=20&sort=-createdAt&role=admin
Never return unbounded lists — page them.
Other essentials:
- Versioning —
/api/v1/...(or a header) so you can evolve without breaking clients. - Statelessness — each request carries its own auth (e.g., a bearer token); no server-side session affinity, which makes horizontal scaling trivial.
- Consistent error shape — e.g.,
{ "error": { "code": "...", "message": "..." } }everywhere. - Validation at the boundary (e.g.,
zod,joi) — reject bad input with400/422before it reaches business logic. - Idempotency —
PUT/DELETEshould be safe to retry; consider idempotency keys forPOST. - Security — auth/authz, rate limiting, HTTPS, and don't leak internal details in errors.