Overview
A concise, practical guide to building backend services with Node.js and Express.js. This covers how to initialize a project, create a server, organize routes, use middleware (built-in and custom), and handle requests and responses with best practices and working examples.
Quick setup
- Initialize a Node.js project and install Express:
mkdir my-express-app
cd my-express-app
npm init -y
npm install express
- Create a basic server in
index.js:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
app.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}`);
});
Open http://localhost:3000 to see the response.
Server setup (practical tips)
- Use environment variables for configuration (
process.env.PORT, database URLs, secrets). - Keep
index.jssmall: delegate routing and middleware to separate modules. - Consider using
nodemonin development for auto-restarts:
npm install --save-dev nodemon
# add script in package.json: "dev": "nodemon index.js"
Core terms & abbreviations
- Node.js โ JavaScript runtime built on Chrome’s V8 engine for running JS on the server.
- Express.js โ Minimal, unopinionated web framework for Node.js that provides routing and middleware primitives.
- Middleware โ Functions that run during the request/response lifecycle (e.g., parsers, auth, loggers).
- Router โ A mini-app that groups related routes using
express.Router(). - CORS โ Cross-Origin Resource Sharing, a browser security feature that servers must opt into.
- JWT โ JSON Web Token, a compact, URL-safe way to represent claims used for stateless authentication.
- PM2 โ A process manager for Node.js for clustering and monitoring in production.
Quick reference: CSR (Client-side Rendering), SSR (Server-side Rendering), SSG (Static-site Generation), CI/CD (Continuous Integration / Delivery).
Routing
Routing is how you map HTTP methods and paths to handlers.
HTTP methods & basic routes
// GET - read data
app.get('/items', (req, res) => { res.json({ items: [] }); });
// POST - create
app.post('/items', (req, res) => { /* create item */ });
// PUT - replace / update
app.put('/items/:id', (req, res) => { /* update item with req.params.id */ });
// DELETE - remove
app.delete('/items/:id', (req, res) => { /* delete item */ });
- Use
req.paramsfor route parameters (e.g.,/items/:id). - Use
req.queryfor query strings (e.g.,/items?limit=10).
Route params & query strings (examples)
// Route parameter example: /items/42
app.get('/items/:id', (req, res) => {
const id = req.params.id; // '42'
res.json({ id });
});
// Query string example: /items?limit=10&page=2
app.get('/items', (req, res) => {
const limit = parseInt(req.query.limit || '10', 10);
const page = parseInt(req.query.page || '1', 10);
res.json({ limit, page });
});
Async routes & error propagation
When using async handlers, catch errors and forward them to your error middleware with next(err) (or use a helper like express-async-handler).
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get('/async-data', asyncHandler(async (req, res) => {
const data = await fetchFromDb(); // may throw
res.json({ data });
}));
Organizing routes
- Create router modules with
express.Router()to group endpoints (e.g.,routes/items.js).
Example routes/items.js:
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => { res.json({ items: [] }); });
router.post('/', (req, res) => {
// create new item
res.status(201).json({ id: 1 });
});
module.exports = router;
Mount routers in index.js:
const itemsRouter = require('./routes/items');
app.use('/items', itemsRouter);
This keeps your app modular and testable.
Middleware
Middleware are functions that run during the request/response lifecycle. They have access to req, res, and next().
Built-in middleware
express.json()โ parses JSON bodies and populatesreq.body.express.urlencoded()โ parses URL-encoded bodies.express.static()โ serves static files.
Example:
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/public', express.static('public'));
Common third-party middleware
// Logging
const morgan = require('morgan');
app.use(morgan('tiny'));
// CORS
const cors = require('cors');
app.use(cors({ origin: 'https://example.com' }));
// Security headers
const helmet = require('helmet');
app.use(helmet());
Custom middleware
A simple logger middleware:
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}
app.use(logger);
// Simple auth middleware (example)
function requireAuth(req, res, next) {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
const token = auth.slice(7);
// verify token (pseudo)
if (token !== process.env.DEMO_TOKEN) return res.status(403).json({ error: 'Forbidden' });
next();
}
app.use('/private', requireAuth, (req, res) => res.json({ secret: 'ok' }));
// Validation middleware (Zod example)
const { z } = require('zod');
const createUserSchema = z.object({ email: z.string().email(), password: z.string().min(6) });
function validate(schema) {
return (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (err) {
return res.status(400).json({ error: err.errors || err.message });
}
};
}
app.post('/users', validate(createUserSchema), (req, res) => {
// req.body is validated
res.status(201).json({ user: req.body.email });
});
// File upload example (multer)
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ filename: req.file.filename, original: req.file.originalname });
});
Middleware order matters โ define global middleware (parsers, loggers, CORS) before routers.
Error-handling middleware
Use a 4-argument middleware to handle errors:
function errorHandler(err, req, res, next) {
console.error(err);
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
}
app.use(errorHandler);
Throw errors in routes or pass to next(err) to trigger this handler.
Keep responsibilities separated and small. A common pattern:
my-express-app/
โโ package.json
โโ src/
โ โโ app.js # create express app, routes, middleware
โ โโ server.js # boot server, clustering, environment setup
โโ routes/
โ โโ items.js
โโ tests/
Example src/app.js:
const express = require('express');
const app = express();
app.use(express.json());
app.use(require('../routes/items'));
module.exports = app;
Example src/server.js (start server, optional cluster):
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Listening ${PORT}`));
Use dotenv in development to load .env files and avoid committing secrets.
For production process management & clustering consider pm2 or Docker+Kubernetes depending on scale.
Request & Response handling
req (request) useful properties
req.paramsโ route parameters (e.g.,{ id: '1' }).req.queryโ query string params (e.g.,{ q: 'search' }).req.bodyโ parsed request body (when using body-parsing middleware).req.headersโ request headers.
res (response) helpful methods
res.send()โ send a string or buffer.res.json()โ send JSON and set content-type.res.status(code)โ set HTTP status code.res.redirect(url)โ redirect to another URL.res.set(header, value)โ set headers.
Example: POST handler that validates input and returns proper status codes
app.post('/signup', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Missing email or password' });
}
// pretend to create user
const user = { id: 1, email };
res.status(201).json({ user });
});
Status code guidance (short)
- 200 OK โ successful GET/PUT/PATCH requests
- 201 Created โ successful POST creates a resource
- 204 No Content โ successful request with no body (e.g., delete)
- 400 Bad Request โ validation error / malformed request
- 401 Unauthorized โ authentication required or failed
- 403 Forbidden โ authenticated but not permitted
- 404 Not Found โ resource not found
- 500 Internal Server Error โ unexpected server error
Security & best practices
- Never trust user input โ validate and sanitize request data.
- Do not store secrets in source code; use environment variables and a secrets manager.
- Use HTTPS in production and HSTS headers.
- Rate-limit and throttle endpoints to reduce abuse (e.g.,
express-rate-limit). - Add input validation libraries (Zod, Joi, Yup) and centralize validation middleware.
- Use helmet for basic security headers:
npm install helmetthenapp.use(require('helmet')());
Development & testing tips
- Use a linter (ESLint) and a test runner (Jest, Vitest).
- Write small modules and unit tests for route handlers and middleware.
- Add integration tests with supertest to assert HTTP behavior.
Example supertest usage (basic):
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/ping', (req, res) => res.send('pong'));
test('ping', async () => {
const res = await request(app).get('/ping');
expect(res.text).toBe('pong');
expect(res.status).toBe(200);
});
Deployment & scaling (text graphs)
Simple hosted web app (static + API):
frontend (static site) -> CDN -> load balancer -> app servers -> database
Containerized app (Docker + Kubernetes):
git -> CI -> Docker image -> Container registry -> Kubernetes cluster -> pods (app) -> service -> DB
Serverless / Functions-first (edge friendly):
git -> CI -> deploy functions (AWS Lambda / Cloudflare Workers / Vercel Functions) -> storage / managed DB
Scaling notes:
- Use a CDN for static assets and offload client-side caching.
- Move CPU-intensive or blocking work to background workers or separate services to avoid blocking the Node.js event loop.
- Add health checks, readiness probes, and graceful shutdown to enable rolling upgrades.
Common pitfalls & best practices
- Blocking the event loop: avoid heavy synchronous operations (large
fs.readFileSync, crypto loops) on request handlers. - Not validating inputs: always validate and sanitize
req.body/req.queryto avoid injection and logic bugs. - Missing error handling for promises: use a safe async wrapper or library so errors don’t crash the process.
- Unbounded request bodies: limit request sizes with express.json({ limit: ‘1mb’ }) to protect memory.
- Overly permissive CORS: be explicit about allowed origins and credentials.
- No observability: add structured logging, request IDs, and metrics (Prometheus) to diagnose issues.
Pros, cons & alternatives
-
Node.js + Express
- Pros: large ecosystem, fast developer iteration, great for JSON APIs and real-time apps (WebSockets).
- Cons: single-threaded by default (need extra care for CPU-bound work), ecosystem can be chaotic with many competing libs.
-
Alternatives
- Python + FastAPI: faster to write typed APIs with Python, async support, very good for scientific stacks.
- Go (Gin/Fiber): high performance, simple concurrency model, compiled binary; better for raw throughput.
- Deno: modern runtime with secure defaults and TypeScript out-of-the-box; smaller ecosystem than Node as of 2025.
- NestJS: a structured, opinionated framework on Node that adds DI and architectural patterns for large teams.
- Serverless Functions (Vercel, Cloudflare Workers): lower ops overhead, better for bursty workloads, but comes with cold-start and execution time limits.
Choose based on team skill, performance needs, and operational constraints.
Quick reference: common snippets
- Body parser:
app.use(express.json()); - Static files:
app.use('/static', express.static('public')) - Router:
const router = express.Router()thenapp.use('/prefix', router) - Error handler:
function errHandler(err, req, res, next) { ... }
Conclusion & next steps
This guide gave you the foundational concepts to start building Express apps: a minimal server, modular routing, middleware patterns, request/response handling, and best practices. Next steps:
- Add authentication (JWT or sessions)
- Integrate a database (Postgres, MongoDB) and ORM (Prisma, Sequelize)
- Add validation and testing around edge cases
- Learn about deploying Node apps (Docker, Heroku, cloud providers)
Further reading
-
Express documentation โ https://expressjs.com/
-
Node.js docs โ https://nodejs.org/en/docs/
-
OWASP API Security โ https://owasp.org/www-project-api-security/
-
Supertest โ https://github.com/visionmedia/supertest
-
Node.js Best Practices (GitHub) โ https://github.com/goldbergyoni/nodebestpractices
-
Express security best practices โ https://expressjs.com/en/advanced/best-practice-security.html
-
Practical Node.js (book) โ https://www.oreilly.com/library/view/practical-nodejs/ (note: check availability)
-
Scaling Node.js articles โ https://www.digitalocean.com/community/tags/nodejs
Comments