What We'll Build

In this tutorial, you'll build a simple but complete REST API for managing a list of tasks. By the end, you'll understand how to set up an Express server, define routes, handle request/response cycles, use middleware, and return proper HTTP status codes.

Prerequisites

  • Node.js (v18+) installed on your machine
  • Basic familiarity with JavaScript
  • A terminal and a code editor

Step 1: Initialize Your Project

Create a new directory and initialize a Node.js project:

mkdir task-api && cd task-api
npm init -y
npm install express

Create the entry file:

touch index.js

Step 2: Set Up the Express Server

Open index.js and add the following boilerplate:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse JSON request bodies
app.use(express.json());

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

Run it with node index.js — you should see the server start message.

Step 3: Create In-Memory Data Storage

For this tutorial we'll use a simple array. In a real app, you'd replace this with a database.

let tasks = [
  { id: 1, title: 'Buy groceries', done: false },
  { id: 2, title: 'Read a book', done: true },
];
let nextId = 3;

Step 4: Define Your Routes

A RESTful API follows predictable URL patterns. Here's the full CRUD implementation:

GET /tasks — List All Tasks

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

GET /tasks/:id — Get a Single Task

app.get('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) return res.status(404).json({ error: 'Task not found' });
  res.json(task);
});

POST /tasks — Create a Task

app.post('/tasks', (req, res) => {
  const { title } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });

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

PATCH /tasks/:id — Update a Task

app.patch('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) return res.status(404).json({ error: 'Task not found' });

  const { title, done } = req.body;
  if (title !== undefined) task.title = title;
  if (done !== undefined) task.done = done;

  res.json(task);
});

DELETE /tasks/:id — Delete a Task

app.delete('/tasks/:id', (req, res) => {
  const index = tasks.findIndex(t => t.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Task not found' });

  tasks.splice(index, 1);
  res.status(204).send();
});

Step 5: Add a Global Error Handler

Always add a catch-all error handler as the last middleware in your app:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong' });
});

REST HTTP Status Code Reference

Status CodeMeaningWhen to Use
200OKSuccessful GET or PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestInvalid input from client
404Not FoundResource doesn't exist
500Internal Server ErrorUnhandled server error

Testing Your API

Use curl or a tool like Postman or Insomnia to test your endpoints:

# Get all tasks
curl http://localhost:3000/tasks

# Create a new task
curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Write tests"}'

Next Steps

You now have a working REST API. From here, you can:

  • Connect a real database like MongoDB or PostgreSQL
  • Add authentication with JWT
  • Add request validation with Zod or Joi
  • Write automated tests with Jest + Supertest