← ALL POSTS
CUSTOM SOFTWARE STEP-BY-STEP GUIDE

Build a REST API with Node.js and Express

A REST API is the plumbing behind most modern software: a server that accepts requests over HTTP and answers with JSON. If you're going to build web apps, internal tools, or integrations, this is the fundamental skill. In this guide you'll build a small but real API with Node.js and Express: it will list items, fetch one by ID, accept new ones via POST, and handle errors cleanly. By the end you'll have a working server you can extend into anything.

Prerequisites

Step 1: Create the project

Make a folder and initialize an npm project inside it. The -y flag accepts the defaults so you don't have to answer prompts.

mkdir todo-api
cd todo-api
npm init -y
npm install express

This creates package.json (your project manifest) and installs Express, the most widely used web framework for Node. That's the only dependency we need.

Step 2: Write a minimal server

Create a file named server.js with this content:

const express = require('express');

const app = express();
const PORT = 3000;

// Parse JSON request bodies
app.use(express.json());

app.get('/', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(PORT, () => {
  console.log(`API listening on http://localhost:${PORT}`);
});

Two things matter here. express.json() is middleware that reads incoming JSON bodies and puts the result on req.body. Without it, POST data is unreadable. And app.listen starts the HTTP server on port 3000.

Run it:

node server.js

Open http://localhost:3000 in a browser or hit it with curl. You should see {"status":"ok"}.

Step 3: Add GET routes

Real APIs serve data. We'll keep ours in an in-memory array so we can focus on the API shape (a database comes later, and slots in cleanly). Add this above the routes in server.js:

let todos = [
  { id: 1, title: 'Buy fittings', done: false },
  { id: 2, title: 'Quote the Henderson job', done: true }
];
let nextId = 3;

Then add two routes:

// List all todos
app.get('/todos', (req, res) => {
  res.json(todos);
});

// Get one todo by ID
app.get('/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) {
    return res.status(404).json({ error: 'Todo not found' });
  }
  res.json(todo);
});

The :id in the path is a route parameter. Express puts it on req.params.id as a string, so we convert it to a number before comparing. Note the 404 with a JSON error body when the ID doesn't exist: an API should always answer in JSON, even for failures.

Step 4: Add a POST route

POST is how clients create data. Add this route:

// Create a todo
app.post('/todos', (req, res) => {
  const { title } = req.body;

  if (!title || typeof title !== 'string') {
    return res.status(400).json({ error: 'title is required' });
  }

  const todo = { id: nextId++, title, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

Validate input before trusting it. Here we require a non-empty string title and answer with 400 (bad request) if it's missing. On success we return 201 (created) and the new object, IDs included, so the client knows what was made.

Step 5: Add error-handling middleware

Express lets you define a catch-all error handler: a function with four arguments, registered after all routes. Anything a route throws (or passes to next()) lands here instead of crashing the server or leaking a stack trace to the client.

// 404 for unknown routes
app.use((req, res) => {
  res.status(404).json({ error: 'Not found' });
});

// Central error handler (must have 4 args)
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

The order matters: routes first, then the 404 handler, then the error handler last. Log the real error server-side, but send the client a generic message. Internal details in error responses are a security problem.

Step 6: Run and test it

Restart the server:

node server.js

Then exercise every route with curl from a second terminal:

curl http://localhost:3000/todos

curl http://localhost:3000/todos/1

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Order parts"}'

curl http://localhost:3000/todos/999

You should see the full list, a single item, a newly created item with id: 3, and a clean 404 error, in that order. Run the list call again and your new item is in it. During development, node --watch server.js restarts the server automatically when you save.

Where to go from here

You have a real API: routes, JSON in and out, validation, and centralized error handling. The pattern scales. Next steps, roughly in order: swap the array for a real database (PostgreSQL with the pg library is our usual pick), add PUT and DELETE routes, and add authentication before anything sensitive goes in.

For deployment, the short version: an in-memory array resets on every restart, so add the database first. Then run the app on a small VPS or a platform like Render, Fly.io, or Railway, set the port from process.env.PORT instead of hardcoding 3000, and put it behind HTTPS. A small API like this runs comfortably on the cheapest tier of any of those platforms.

Stuck on this, or want it done for you? That's the job.

Email us →
RELATED READING
Build your first LLM feature with the Claude API Step-By-Step Guide
Rescuing legacy software: modernize without the rewrite Explainer
Python: the duct tape of business automation Explainer
REST or GraphQL: how we choose Explainer
System integrations: make your apps talk to each other Explainer
NO FORMS. JUST EMAIL.
mason@hurbs.io
or (832) 457-4317, LA and Houston