
REST is a set of conventions for designing APIs that are predictable, consistent, and easy to work with. This post covers what REST means, how it maps to HTTP, and how to build a clean resource-based API in Express.
Every application that talks to a server is using an API. The browser fetches a webpage. A mobile app loads a user's feed. A dashboard pulls analytics. In all these cases, the client sends a request and the server sends back data. REST is a set of conventions that make this communication consistent, so any client can talk to any server without needing custom knowledge of how that server was built.
REST stands for Representational State Transfer. Ignore the acronym. What it really means in practice: your API is organized around resources, and you interact with those resources using standard HTTP methods.
A resource is a noun: a user, a post, a product, an order. The API exposes endpoints that represent these things, and HTTP methods represent what you want to do with them. The method tells the server your intent. The URL tells the server what you're talking about.
What you're doing HTTP Method What you're saying ───────────────────────────────────────────────────── Read data GET "Give me this resource" Create data POST "Here's a new resource, save it" Update data PUT/PATCH "Here's the updated version" Delete data DELETE "Remove this resource"
This is the entire foundation. Every URL pattern and every convention in REST flows from this.
The first rule: URLs should be nouns, not verbs.
Wrong (verb-based): Right (resource-based): GET /getUsers GET /users POST /createUser POST /users GET /getUserById?id=42 GET /users/42 POST /deleteUser DELETE /users/42 POST /updateUser PUT /users/42
The URL identifies the resource. The HTTP method describes the action. Mixing them means your API becomes a custom language every consumer has to learn instead of a standard one they already know.
Use plurals for collections. /users refers to the collection. /users/42 refers to one user in that collection.
GET: Retrieve data. Safe and idempotent, meaning calling it multiple times has no side effects and gives the same result. Never use GET to modify data.
POST: Create a new resource. The request body contains the data for the new resource. Returns the created resource and a 201 Created status.
PUT: Replace a resource completely. The request body should contain the full updated resource. Returns the updated resource.
PATCH: Partially update a resource. Only send the fields you want to change. Useful when updating one field without overwriting others.
DELETE: Remove a resource. Returns 204 No Content on success (resource is gone, nothing to return).
Status codes tell the client what happened. There are five ranges:
┌─────────────────────────────────────────────────────┐ │ Status Code Ranges │ ├──────────┬──────────────────────────────────────────┤ │ 2xx │ Success │ │ 200 OK │ Request succeeded, data in body │ │ 201 Created │ New resource created │ │ 204 No Content │ Success, no body to return │ ├──────────┼──────────────────────────────────────────┤ │ 4xx │ Client error (wrong request) │ │ 400 Bad Request │ Invalid data sent │ │ 401 Unauthorized │ Not logged in │ │ 403 Forbidden │ Logged in but no permission │ │ 404 Not Found │ Resource doesn't exist │ ├──────────┼──────────────────────────────────────────┤ │ 5xx │ Server error (something broke) │ │ 500 Internal Server Error │ Generic server fault │ └──────────┴──────────────────────────────────────────┘
The most common mistake: returning 200 OK for everything, including errors. The status code is information. Clients and tooling rely on it for error handling, logging, and retry logic. Use the right code.
Here's the full lifecycle:
Client Express Server │ │ │ GET /users │ │─────────────────────────────────────────►│ fetch all users │ 200 OK + users array │ │◄─────────────────────────────────────────│ │ │ │ POST /users { name, email } │ │─────────────────────────────────────────►│ validate + save │ 201 Created + new user object │ │◄─────────────────────────────────────────│ │ │ │ GET /users/42 │ │─────────────────────────────────────────►│ find user 42 │ 200 OK + user object │ │◄─────────────────────────────────────────│ │ │ │ PUT /users/42 { name, email } │ │─────────────────────────────────────────►│ replace user 42 │ 200 OK + updated user │ │◄─────────────────────────────────────────│ │ │ │ DELETE /users/42 │ │─────────────────────────────────────────►│ delete user 42 │ 204 No Content │ │◄─────────────────────────────────────────│
Now in Express:
javascriptLoading syntax highlighter...
┌────────────┬───────────┬─────────────────┬──────────────┐ │ Operation │ Method │ Route │ Status │ ├────────────┼───────────┼─────────────────┼──────────────┤ │ Read all │ GET │ /users │ 200 │ │ Read one │ GET │ /users/:id │ 200 / 404 │ │ Create │ POST │ /users │ 201 / 400 │ │ Update │ PUT │ /users/:id │ 200 / 404 │ │ Delete │ DELETE │ /users/:id │ 204 / 404 │ └────────────┴───────────┴─────────────────┴──────────────┘
This pattern is repeatable across every resource. Swap users for posts, products, or orders and the exact same structure applies.
When one resource belongs to another, nest the route:
GET /users/:userId/posts → all posts by user GET /users/:userId/posts/:postId → one specific post POST /users/:userId/posts → create a post for user
Don't nest more than two levels deep. It gets hard to read and usually means you can restructure instead.
/users, /posts, /products. Dynamic segments use :id.201 for created, 204 for deleted, 400 for bad input, 404 for not found. Never return 200 for errors.REST conventions exist because predictability has real value. A developer who has used one REST API can figure out any other REST API with minimal documentation. That's the actual point: not the acronym, not the architecture paper, just a shared language between clients and servers.
Related posts based on tags, category, and projects
Express.js is a minimal web framework for Node.js that takes the verbosity out of building HTTP servers. This post covers what Express adds over raw Node, how routing works, and how to handle GET and POST requests cleanly.
URL parameters and query strings both live in the URL, but they serve very different purposes. This post breaks down what each one is, how to access them in Express.js, and when to use which.
Middleware is code that runs between a request arriving and a response being sent. In Express, every middleware function in the chain gets a chance to inspect, modify, or stop a request. This post covers what middleware is, how `next()` controls the flow, and where it gets used in real applications.
File uploads arrive at your server in a format called multipart/form-data that Express can't parse on its own. Multer is the middleware that bridges that gap. This post covers how multer works, how to handle single and multiple files, and how to configure storage.
const express = require("express");
const app = express();
app.use(express.json());
// In-memory store for demonstration
let users = [{ id: 1, name: "Atharv", email: "atharv@example.com" }];
// GET all users
app.get("/users", (req, res) => {
res.json(users);
});
// GET one user
app.get("/users/:id", (req, res) => {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: "User not found" });
res.json(user);
});
// POST create user
app.post("/users", (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "name and email are required" });
}
const newUser = { id: Date.now(), name, email };
users.push(newUser);
res.status(201).json(newUser);
});
// PUT update user
app.put("/users/:id", (req, res) => {
const index = users.findIndex((u) => u.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: "User not found" });
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "name and email are required" });
}
users[index] = { ...users[index], name, email };
res.json(users[index]);
});
// DELETE user
app.delete("/users/:id", (req, res) => {
const index = users.findIndex((u) => u.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: "User not found" });
users.splice(index, 1);
res.status(204).send();
});
app.listen(3000);