Skip to main content
โšก Calmops

Node.js & Express Basics โ€” Server setup, routing, middleware, request/response handling

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

  1. Initialize a Node.js project and install Express:
mkdir my-express-app
cd my-express-app
npm init -y
npm install express
  1. 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.js small: delegate routing and middleware to separate modules.
  • Consider using nodemon in 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.params for route parameters (e.g., /items/:id).
  • Use req.query for 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 populates req.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 helmet then app.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.query to 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() then app.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

Comments