Skip to content

Building a CRUD API With Node.js

CRUD, Create, Read, Update, Delete, is the standard set of operations almost every resource needs. This lesson builds a complete CRUD API for a single resource using Express.

The Four CRUD Operations

Create adds a new resource (POST), Read retrieves one or many resources (GET), Update modifies an existing resource (PUT/PATCH), and Delete removes a resource (DELETE). Together, these four operations cover almost everything a typical resource-oriented API needs to support.

The example below uses an in-memory array to keep the focus on the API shape itself; the Databases lessons later in this course show how to swap that array for a real persistent store without changing the route structure.

import express from 'express';
const app = express();
app.use(express.json());

let notes = [];
let nextId = 1;

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

app.post('/notes', (req, res) => {
  const note = { id: nextId++, text: req.body.text };
  notes.push(note);
  res.status(201).json(note);
});

app.put('/notes/:id', (req, res) => {
  const note = notes.find((n) => n.id === Number(req.params.id));
  if (!note) return res.status(404).json({ error: 'Not found' });
  note.text = req.body.text;
  res.json(note);
});

app.delete('/notes/:id', (req, res) => {
  notes = notes.filter((n) => n.id !== Number(req.params.id));
  res.status(204).end();
});

res.status(204).end() on delete sends no response body, the conventional way to acknowledge a successful deletion.

CRUD Route Mapping

Create -> POST   /notes
Read   -> GET    /notes        (all)
         GET    /notes/:id     (one)
Update -> PUT    /notes/:id
Delete -> DELETE /notes/:id
  • Create returns the newly created resource with a 201 status.
  • Read operations never modify data and should be safe to retry.
  • Update operations should return the updated resource, not just a success flag.
  • Delete operations conventionally return 204 No Content with an empty body.

CRUD API Cheatsheet

Response expectations for each CRUD operation.

Operation Method Typical Response
Create POST 201 + the created resource
Read (list) GET 200 + an array of resources
Read (single) GET 200 + the resource, or 404 if missing
Update PUT/PATCH 200 + the updated resource, or 404 if missing
Delete DELETE 204 with no body, or 404 if missing

Idempotency in CRUD Operations

An idempotent operation produces the same end result no matter how many times it's repeated. GET, PUT, and DELETE are conventionally idempotent, calling DELETE /notes/5 twice should leave the system in the same state as calling it once. POST is intentionally not idempotent, calling it twice typically creates two resources.

Consistently Handling Missing Resources

Every operation that targets a specific resource by ID (GET /notes/:id, PUT /notes/:id, DELETE /notes/:id) needs a consistent way to respond when that ID doesn't exist, typically 404 Not Found with a clear error message.

function findNoteOr404(req, res, notes) {
  const note = notes.find((n) => n.id === Number(req.params.id));
  if (!note) {
    res.status(404).json({ error: `Note ${req.params.id} not found` });
    return null;
  }
  return note;
}

Extracting this lookup into a small helper avoids repeating the same not-found logic across every ID-based route.

Common Mistakes

  • Returning 200 OK for a delete instead of 204 No Content (or 200 with a confirmation body, chosen consistently).
  • Forgetting to check whether a resource exists before updating or deleting it, silently succeeding on invalid IDs.
  • Not returning the updated resource from an update operation, forcing clients to re-fetch it separately.
  • Treating POST as idempotent and relying on repeated calls not to create duplicates.

Key Takeaways

  • CRUD, Create, Read, Update, Delete, maps cleanly onto POST, GET, PUT/PATCH, and DELETE.
  • Each operation has a conventional success status code and response shape.
  • GET, PUT, and DELETE are conventionally idempotent; POST is not.
  • Consistent 404 handling for missing resources should apply across every ID-based route.

Pro Tip

Build your CRUD routes against an in-memory array first, exactly as shown here, before wiring up a real database. It lets you validate the API's shape and status codes quickly, then swap in persistence later with minimal route changes.