Skip to content

Building a CRUD API with Express

CRUD, Create, Read, Update, Delete, is the foundation almost every REST resource is built on. This lesson builds a complete CRUD API for a tasks resource using Express, starting with an in-memory array.

The Four CRUD Operations

Every CRUD resource needs, at minimum, a way to list and read records (Read), add new ones (Create), modify existing ones (Update), and remove them (Delete). In Express, each operation maps to one HTTP method and route.

The example below uses a plain in-memory array so you can focus purely on the routing and logic; the same shape applies once you swap in a real database (see the Databases section).

let tasks = [{ id: 1, title: 'Learn Express', done: false }];
let nextId = 2;

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

app.post('/tasks', (req, res) => {
  const task = { id: nextId++, title: req.body.title, done: false };
  tasks.push(task);
  res.status(201).json(task);
});

GET /tasks reads the whole collection; POST /tasks creates a new task and returns it with a 201 Created status.

Full CRUD Route Set

GET    /tasks       list all tasks
GET    /tasks/:id   get one task
POST   /tasks       create a task
PUT    /tasks/:id   replace a task
PATCH  /tasks/:id   update part of a task
DELETE /tasks/:id   delete a task
  • Each route corresponds to exactly one CRUD operation.
  • Always validate that a :id resource exists before updating or deleting it, returning 404 otherwise.
  • Return the created/updated resource in the response body so the client sees the final state.
  • DELETE typically returns 204 No Content with an empty body.

CRUD API Cheatsheet

The full route table for a typical CRUD resource.

Operation Method + Path Body Required Response
List GET /tasks No Array of tasks
Read one GET /tasks/:id No Single task or 404
Create POST /tasks Yes Created task, 201
Replace PUT /tasks/:id Yes Updated task
Update PATCH /tasks/:id Yes (partial) Updated task
Delete DELETE /tasks/:id No 204 No Content

Full In-Memory CRUD Example

Here is a complete router implementing all five routes for the tasks resource, including 404 handling.

const express = require('express');
const router = express.Router();

let tasks = [{ id: 1, title: 'Learn Express', done: false }];
let nextId = 2;

router.get('/', (req, res) => res.json(tasks));

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

router.post('/', (req, res) => {
  const task = { id: nextId++, title: req.body.title, done: false };
  tasks.push(task);
  res.status(201).json(task);
});

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

router.delete('/:id', (req, res) => {
  const exists = tasks.some((t) => t.id === Number(req.params.id));
  if (!exists) return res.status(404).json({ error: 'Task not found' });
  tasks = tasks.filter((t) => t.id !== Number(req.params.id));
  res.status(204).end();
});

module.exports = router;

Moving to a Real Database

Swapping the in-memory array for a real database changes only the implementation inside each route, tasks.find(...) becomes Task.findById(...), the route signatures and HTTP contract stay exactly the same.

Common Mistakes

  • Not checking whether a resource exists before updating or deleting it, silently "succeeding" on nothing.
  • Using PUT semantics (full replace) but only updating the fields present in the body, corrupting the rest of the resource.
  • Returning 200 OK for a delete when 204 No Content better matches the empty response body.
  • Storing state in a plain in-memory array in production, it resets on every restart and doesn't scale across multiple server instances.

Key Takeaways

  • CRUD maps directly onto GET, POST, PUT/PATCH, and DELETE routes.
  • Always check for resource existence before update/delete, responding 404 when missing.
  • The in-memory array pattern is great for learning, but must be replaced with a real database for production.
  • Route shapes stay stable even as the underlying data layer changes.

Pro Tip

Build and test your CRUD routes against an in-memory array first, once all the HTTP behavior and status codes are correct, swapping in a real database becomes a small, isolated change.