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
- Node.js 20 or newer installed. Check with
node --version. If you don't have it, grab the LTS installer from the official Node.js site. - A terminal and a text editor. We'll use plain commands that work on Mac, Linux, or Windows.
- Basic JavaScript familiarity: functions, objects, arrays.
- Optional but handy:
curlfor testing, which ships with most systems.
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 →