Free to list, always.No paid rankings. Every recommendation explains its trade-offs.
OpenSourceChoice
Backend · Intermediate

Creating a RESTful API with Node.js and Express

Build and test a REST API with Node.js and Express using structured routes, validation, environment configuration, and reliable error handling.

3 hoursUpdated Jul 21, 2026543 views

Build a small Express REST API with validation-minded routes, environment config, and clear error responses.

What you will learn

  • Project layout for an Express API
  • CRUD routes with JSON bodies
  • Central error handling and status codes
  • How to keep secrets out of source

Prerequisites

  • Node.js 20+
  • Basic HTTP knowledge (methods, status codes, JSON)

Scaffold

mkdir express-api && cd express-api
npm init -y
npm pkg set type=module
npm install express dotenv zod
npm install -D nodemon

Add "start": "node server.js" and "dev": "nodemon server.js" to package.json. Express docs: Express routing.

Minimal server

import "dotenv/config";
import express from "express";
import { z } from "zod";

const app = express();
app.use(express.json());

const items = new Map();
const createItem = z.object({ name: z.string().trim().min(1).max(100) });

app.get("/health", (_req, res) => res.json({ ok: true }));

app.get("/items", (_req, res) => {
  res.json([...items.values()]);
});

app.get("/items/:id", (req, res) => {
  const item = items.get(req.params.id);
  if (!item) return res.status(404).json({ error: "not_found" });
  res.json(item);
});

app.post("/items", (req, res) => {
  const parsed = createItem.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
  const item = { id: crypto.randomUUID(), name: parsed.data.name };
  items.set(item.id, item);
  res.status(201).json(item);
});

app.delete("/items/:id", (req, res) => {
  if (!items.delete(req.params.id)) return res.status(404).json({ error: "not_found" });
  res.status(204).end();
});

app.use((error, _req, res, _next) => {
  console.error(error);
  res.status(500).json({ error: "internal_error" });
});

const port = Number(process.env.PORT || 3000);
app.listen(port, () => console.log(`listening on ${port}`));

Status codes to use deliberately

  • 200 / 201 / 204 for success
  • 400 for invalid input
  • 404 when the resource is missing
  • 500 only for unexpected failures

Practice checklist

  • Implement PATCH /items/:id with Zod
  • Move routes into routes/items.js
  • Add a tiny request logger middleware
  • Hit the API with curl or REST Client and document examples in README

Common pitfalls

  • Swallowing errors without status codes
  • Trusting client IDs without checks
  • Hardcoding database URLs in source
  • Forgetting express.json() and wondering why req.body is empty

Sources and further reading

Who this guide is for

  • Backend developers building their first production-shaped API
  • Frontend developers learning HTTP contracts
  • Teams standardizing small Node.js services

Definition of done

A versioned Express API with validated input, consistent errors, request tracing, automated route tests, health signals, and a documented runbook for configuration and shutdown.

Finishing the commands is not the same as finishing the guide. Keep the result only when another person can reproduce it, inspect the evidence, and understand the remaining risks.

Professional workflow

Phase 1 — Write the contract first

Goal: Make behavior reviewable before implementation grows.

  • List resources, methods, status codes, and example payloads.
  • Define validation and authorization boundaries.
  • Choose one consistent error envelope.

Verification: A client developer can implement against examples without reading server code.

Phase 2 — Separate transport from domain logic

Goal: Keep routes thin and testable.

  • Parse and validate at the HTTP boundary.
  • Move business rules into functions that do not depend on Express objects.
  • Centralize not-found and unexpected-error handling.

Verification: Core rules can be tested without opening a network port.

Phase 3 — Operate the service

Goal: Make success and failure observable.

  • Add request IDs and structured logs.
  • Expose liveness separately from dependency readiness.
  • Handle shutdown so active requests finish within a bounded period.

Verification: A test proves the service rejects new work and exits cleanly during shutdown.

Review questions before you continue

  • Which errors are client-correctable?
  • Where is authentication enforced?
  • What is the idempotency behavior of retries?
  • Which dependency failures should mark readiness unhealthy?

If an answer is unknown, record it as an explicit follow-up instead of hiding the uncertainty behind a successful demo.

Troubleshooting decision guide

  • req.body is undefined: verify content type, JSON middleware, and request size limits.
  • Errors bypass the handler: ensure asynchronous failures reach the error boundary.
  • Clients disagree on status handling: update the contract and add response tests.
  • The process hangs during deploy: close listeners and dependency pools on termination signals.

Change one variable at a time, preserve the first useful error, and write down the command or condition that fixed it. That turns a private debugging session into team knowledge.

Production-readiness checklist

  • Set request-body and timeout limits.
  • Validate configuration at startup.
  • Use structured logs without secrets or raw credentials.
  • Apply authentication, authorization, rate limits, and CORS deliberately.
  • Test dependency outages and graceful shutdown.

These checks are intentionally stricter than a beginner demo. Use them to decide whether to continue learning, run a controlled pilot, or request specialist review.

Your next 30 minutes

  • Write a contract test for one success and three failure cases.
  • Add request IDs and a redacted structured log.
  • Document readiness and shutdown behavior.
  • Compare the result with the official Express production guidance.

CTA: Pick the first unchecked step, complete it now, and save the evidence in your project README or decision log. Then open the official documentation linked below before adopting the result in production.

Similar guides

Keep learning in the same lane.

Matched by technology, category, tags, and connected software—not ranked sponsorship.

AdvancedAuthentication with Node.js

Implement password hashing and JWT authentication in Node.js, then evaluate sessions, secret handling, authorization, and production risks.

Node.js · 3 hours
Browse the full library →
Turn reading into evidence

Make this guide useful today.

Complete one verification step, save the result in your project notes, and decide whether you are ready for the next phase. A reproducible result is more valuable than a finished page.

  • Run the smallest working example.
  • Test one failure or edge case.
  • Record the command, result, and remaining risk.