Designing a small REST API

Routes, HTTP status codes, and input validation for a clean Express API on Falix — the shape most bot dashboards, mobile backends, and side projects actually need, verified end to end.

Most backends you'll build are a REST API: a set of URLs that return JSON instead of HTML. A Discord bot's dashboard, a mobile app's server, a public data feed — all the same shape. This guide covers the three things that separate a tidy API from a messy one: routes that read well, status codes that tell the truth, and validation that rejects bad input before it reaches your logic. It runs on the Node.js application with Express.

At a glance
You need A server running the Node.js application
Background Your first web app (the SERVER_PORT / 0.0.0.0 rule) and Build an Express website + API
Plan Any plan
Time Thirty minutes

New to Express here? Read Build an Express website + API first — this guide assumes the index.js entry file and automatic npm install it explains.

Design the routes first

A REST API maps things (resources) to URLs, and actions to HTTP methods. Pick a plural noun for the resource and let the method carry the verb — never put the verb in the URL.

Do this Not this
GET /api/notes GET /api/getAllNotes
GET /api/notes/42 GET /api/note?id=42&action=fetch
POST /api/notes POST /api/createNote
DELETE /api/notes/42 POST /api/deleteNote

The pattern is always method + collection + optional id. Read it aloud: "GET notes" lists them, "POST notes" adds one, "DELETE notes 42" removes one. Once the shape is consistent, anyone can guess the rest.

Status codes that tell the truth

The status code is the first thing a client reads. Sending 200 OK on a failure is the single most common API bug — it forces every caller to dig through the body to find out what really happened. Use the small set that covers almost everything:

Code Means Use it when
200 OK Success, here's the data A GET that found something
201 Created Made a new thing A POST that added a row
204 No Content Success, nothing to return A DELETE that worked
400 Bad Request Your request was malformed Missing/garbage input
401 Unauthorized You're not logged in No/invalid credentials — see Sessions, JWT & passwords
404 Not Found That thing doesn't exist An id that isn't there
422 Unprocessable Entity Valid shape, invalid values Validation failed
429 Too Many Requests Slow down Rate limited — see Rate limiting your API
500 Internal Server Error You broke, not them An unhandled exception

Validate before you trust

Never write a request body straight into your data. Check it first, and reject bad input with a clear message and a 4xx code. The express-validator library does this cleanly, but the principle is the same if you hand-roll the checks: validate, and only then act.

Install express, then express-validator, from the Packages page (search each name, install, then restart). Here's a complete index.js — a notes API with all four routes, honest status codes, and validation on the one route that takes input:

const express = require('express');
const { body, validationResult } = require('express-validator');

const app = express();
app.use(express.json()); // parse JSON request bodies

let notes = [{ id: 1, title: 'First note', body: 'hello' }];
let nextId = 2;

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

// Get one — 404 if it isn't there
app.get('/api/notes/:id', (req, res) => {
  const note = notes.find((n) => n.id === Number(req.params.id));
  if (!note) return res.status(404).json({ error: 'Note not found' });
  res.json(note);
});

// Create one — validate, then 201
app.post('/api/notes',
  body('title').isString().trim().notEmpty().withMessage('title is required'),
  body('body').isString().trim().notEmpty().withMessage('body is required'),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    const note = { id: nextId++, title: req.body.title, body: req.body.body };
    notes.push(note);
    res.status(201).json(note);
  }
);

// Delete one — 204 on success, 404 if missing
app.delete('/api/notes/:id', (req, res) => {
  const idx = notes.findIndex((n) => n.id === Number(req.params.id));
  if (idx === -1) return res.status(404).json({ error: 'Note not found' });
  notes.splice(idx, 1);
  res.status(204).end();
});

const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));

express.json() turns the incoming JSON body into req.body. Each body('title')... line is a rule; validationResult(req) collects any failures so you can return them all at once with a 422. Every route ends in exactly one response.

Verify it works

Start the server, then hit it from the Console's neighbour — your own machine — using your server's public address (Network page). The shape you should see:

Request Response
GET /api/notes 200 + the array
GET /api/notes/999 404 + {"error":"Note not found"}
POST /api/notes with {"title":"Hi","body":"there"} 201 + the new note
POST /api/notes with {"title":""} 422 + the validation errors
DELETE /api/notes/1 204, empty body

From a terminal, curl proves it in one line each:

curl -i -X POST -H "Content-Type: application/json" \
  -d '{"title":"Hi","body":"there"}' http://YOUR_ADDRESS:PORT/api/notes

The -i flag prints the status line, so you can confirm HTTP/1.1 201 Created with your own eyes.

🎯 Good to know: This example keeps notes in a plain array, so they reset on every restart. That's fine for learning the shape — when you want them to persist, move the array to SQLite or a managed database. Add a database is the next step, and The classic todo REST API is this same design wired to real storage.

A few habits worth keeping

  • One response per request. Every path through a handler must end in exactly one res.json / res.send / res.status().end(). return the early ones so code doesn't fall through and try to respond twice (ERR_HTTP_HEADERS_SENT).
  • Return errors as JSON too. A client parsing your success as JSON will choke on an HTML error page. Send { error: '...' } with the right code.
  • Version later, not never. When an API has real users, prefixing routes with /api/v1/ lets you change things without breaking them.
  • CORS is separate. If a website on another domain calls this API from the browser, you'll hit a CORS wall until you add the cors middleware — see CORS errors explained.

Everything past this point — pagination, filtering, nested resources, OpenAPI docs — is standard Express and standard REST. The official Express guide at expressjs.com covers routing and middleware in depth, and express-validator.github.io documents every validation rule.


Next steps

Was this guide helpful?