Suggest an editImprove this articleRefine the answer for “REST API design principles and best practices?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**REST API design** means exposing data as resources at noun-based URLs (`/users/123`) and using HTTP methods as actions: `GET` reads, `POST` creates, `PUT`/`PATCH` updates, `DELETE` removes. Every request is stateless. ```javascript GET /users // 200 [{id:1,...}] POST /users // 201 {id:2,...} + Location header PATCH /users/123 // 200 {id:123,...} DELETE /users/123 // 204 No Content ``` **Key rule:** plural nouns in paths, HTTP methods as verbs, correct status codes, pagination on list endpoints.Shown above the full answer for quick recall.Answer (EN)Image**REST API design** is a set of conventions for building web services where data is exposed as addressable resources at predictable URLs, and HTTP methods define what you do with them. ## Theory ### TL;DR - Think of it like a library catalog: each book (resource) has a fixed shelf location (URL), and you borrow or return it using standard actions (GET/POST), no special instructions needed. - Core rule: nouns in paths (`/users`), verbs via HTTP (`GET` lists, `POST` creates, `DELETE` removes). - `GET /users/123` reads a user. `DELETE /users/123` removes it. The URL never changes, only the method. - Use REST for public APIs where caching and predictability matter. Use GraphQL when clients need flexible field selection. - Stateless means every request carries its full context. No server-side sessions. ### Quick example ```javascript const express = require('express'); const app = express(); app.use(express.json()); let users = [{ id: 1, name: 'Alice' }]; app.get('/users', (req, res) => res.json(users)); // 200 [{id:1,name:'Alice'}] app.post('/users', (req, res) => { // 201 {id:2,name:'Bob'} const user = { id: users.length + 1, ...req.body }; users.push(user); res.status(201).set('Location', `/users/${user.id}`).json(user); }); app.get('/users/:id', (req, res) => { // 200 or 404 const user = users.find(u => u.id === +req.params.id); user ? res.json(user) : res.status(404).json({ error: 'Not found' }); }); app.delete('/users/:id', (req, res) => { // 204 No Content users = users.filter(u => u.id !== +req.params.id); res.status(204).send(); }); app.listen(3000); ``` `POST` returns `201 Created` with a `Location` header pointing to the new resource. `DELETE` returns `204` with no body. These are not optional conventions, they are what HTTP clients and CDNs expect. ### Key difference from RPC-style APIs RPC-style APIs use verb-heavy paths: `/getUserById`, `/createUser`, `/deleteUser`. REST treats the URL as a noun and the HTTP method as the verb. `/users/123` is the resource. `GET`, `PUT`, and `DELETE` are the actions. This separation means caching infrastructure (CDNs, reverse proxies) can make decisions based on the method alone, without parsing the URL for intent. ### When to use REST - **Public consumer API** - REST. Predictable, cacheable, works with any HTTP client. - **Internal microservice with complex contracts** - REST or gRPC. REST if HTTP tooling is enough. - **Client needs flexible field selection** - GraphQL. Avoids sending 40 fields when you need 3. - **Real-time push updates** - WebSockets or SSE. REST has no push by design. - **Simple CRUD** - REST. No learning curve, works immediately. ### How HTTP handles REST requests The server (Node.js, Nginx, or whatever sits in front) parses incoming requests by matching the URL path to a route handler and dispatches based on `req.method`. No server-side session is stored. Each request carries its full context: auth token in `Authorization`, filters in query params, body in the JSON payload. GET responses can be cached by CDNs using `ETag` and `Last-Modified` headers. When a client sends `If-None-Match` with the stored ETag, the server returns `304 Not Modified` with no body if nothing changed. That is free performance with zero extra code. ### Common mistakes **Verb paths.** `/getUsers` or `/deleteUser/123` breaks caching and confuses HTTP-aware tools. ```javascript // Wrong app.get('/getAllUsers', handler); app.get('/deleteUser/:id', handler); // Right app.get('/users', handler); app.delete('/users/:id', handler); ``` **Singular nouns.** `/user` instead of `/users` creates ambiguity. `POST /user` - does that create a new user or refer to the current one? Always use plural. **Wrong status codes on create.** Returning `200` when a resource is created misleads clients and automated tools. ```javascript // Wrong res.status(200).json(newUser); // Right res.status(201).set('Location', `/users/${newUser.id}`).json(newUser); ``` **No pagination on list endpoints.** A `GET /users` that returns every record will fail on any real dataset. Default to `?page=1&limit=20`. **Stateful sessions.** Storing session data server-side violates statelessness and makes horizontal scaling harder. Use JWT in `Authorization: Bearer <token>` instead. **Confusing PUT and PATCH.** `PUT` replaces the entire resource. If you send only `{ name: 'Bob' }` to `PUT /users/1` and the handler does a full replace, the user's email disappears. `PATCH` is for partial updates. Many teams default to `PATCH` for edit operations and reserve `PUT` for cases where a full replacement is intentional. ### Real-world usage - **Stripe API** - `/v1/customers`, `GET` lists with cursor pagination, `POST` creates with idempotency keys in headers. - **GitHub API** - `/repos/{owner}/{repo}/issues`, HATEOAS-style `_links` for pagination in responses. - **Twitter API v2** - `/2/tweets?expansions=author_id`, query params for field selection. - **Express + Prisma** - plural nouns, HTTP methods, `prisma.user.findMany()` maps directly to `GET /users`. - **Versioning in production** - URL-based (`/api/v1/users`) is simpler for proxy configuration. Header-based (`Accept: application/vnd.myapi.v2+json`) is cleaner but harder to test in a browser. ### Follow-up questions **Q:** Why plural nouns instead of singular? **A:** Plural maps naturally to collections. `POST /users` creates a user and returns `/users/123`. With `/user`, it is unclear whether you are creating a new user or referring to the current one. **Q:** What HTTP status codes go with each CRUD operation? **A:** `GET` returns `200` (found) or `404` (not found). `POST` returns `201 Created` with `Location`. `PUT` and `PATCH` return `200` (with body) or `204` (no body). `DELETE` returns `204 No Content`. **Q:** What is the difference between PUT and PATCH? **A:** `PUT` replaces the entire resource, `PATCH` applies partial changes. GitHub uses `PATCH /repos/:id` to update just the description without overwriting other fields. **Q:** How does API versioning work? **A:** Two main options: URL prefix (`/api/v1/users`) and header-based (`Accept: application/vnd.myapi.v2+json`). URL versioning is easier to route and debug. Header versioning is cleaner but requires client-side configuration. **Q:** How does caching work with REST? **A:** The server sets an `ETag` (a hash of the response) on GET responses. The client stores it and sends `If-None-Match: <etag>` on the next request. If nothing changed, the server returns `304 Not Modified` with no body. CDNs apply the same logic automatically for public endpoints. **Q:** (Senior) Design a REST API for a blog with posts, comments, search, and auth. What endpoints, headers, and status codes would you use? **A:** Start with `GET /posts?page=1&search=foo` returning `{ data: [], _links: { next: { href: '...' } } }`. Auth via `Authorization: Bearer <jwt>`. Create with `POST /posts` returning `201` plus `Location`. Comments at `GET /posts/:id/comments`. Rate limiting via `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers. Include HATEOAS links in each response so clients can navigate without hardcoding URLs. ## Examples ### Basic CRUD for a user resource ```javascript const express = require('express'); const app = express(); app.use(express.json()); let users = [ { id: 1, name: 'Alice', email: 'alice@example.com' } ]; // GET /users - list all app.get('/users', (req, res) => res.json(users)); // GET /users/:id - get one app.get('/users/:id', (req, res) => { const user = users.find(u => u.id === +req.params.id); user ? res.json(user) : res.status(404).json({ error: 'User not found' }); }); // POST /users - create app.post('/users', (req, res) => { const user = { id: users.length + 1, ...req.body }; users.push(user); res.status(201).set('Location', `/users/${user.id}`).json(user); }); // PATCH /users/:id - partial update app.patch('/users/:id', (req, res) => { const user = users.find(u => u.id === +req.params.id); if (!user) return res.status(404).json({ error: 'Not found' }); Object.assign(user, req.body); // merge changes, not replace res.json(user); }); // DELETE /users/:id app.delete('/users/:id', (req, res) => { users = users.filter(u => u.id !== +req.params.id); res.status(204).send(); }); app.listen(3000); ``` Each method maps to one action. The URL (`/users/:id`) stays the same, only the HTTP method changes. `PATCH` uses `Object.assign` to merge changes rather than overwrite. `DELETE` returns `204` with no body, because there is nothing left to describe. ### Pagination and filtering ```javascript app.get('/users', (req, res) => { const { page = 1, limit = 10, name } = req.query; let filtered = users; if (name) filtered = filtered.filter(u => u.name.includes(name)); const start = (page - 1) * limit; const data = filtered.slice(start, +start + +limit); res.json({ data, pagination: { page: +page, limit: +limit, total: filtered.length, pages: Math.ceil(filtered.length / limit) } }); }); // curl "localhost:3000/users?page=1&limit=2&name=Alice" // -> { data: [{id:1,name:'Alice',...}], pagination: {page:1,limit:2,total:1,pages:1} } ``` Never return an unbounded list. Any endpoint that hits a database table without `LIMIT` will fail under load. This pattern matches what Stripe and GitHub use: a `data` array, pagination metadata, and optional filtering via query params. I have seen this exact bug, a `GET /orders` that worked fine in development and timed out on the first Monday after launch. ### HATEOAS links and partial update with JSON Patch ```javascript // PATCH with JSON Patch (RFC 6902) - array of operations app.patch('/users/:id', (req, res) => { const user = users.find(u => u.id === +req.params.id); if (!user) return res.status(404).json({ error: 'User not found' }); // req.body: [{ op: 'replace', path: '/name', value: 'Alicia' }] req.body.forEach(op => { if (op.op === 'replace') user[op.path.slice(1)] = op.value; }); res.json({ ...user, _links: { self: { href: `/users/${user.id}` }, posts: { href: `/users/${user.id}/posts` } } }); }); // curl -X PATCH localhost:3000/users/1 \ // -H "Content-Type: application/json" \ // -d '[{"op":"replace","path":"/name","value":"Alicia"}]' // -> { id: 1, name: 'Alicia', _links: { self: {href:'/users/1'}, posts: {href:'/users/1/posts'} } } ``` JSON Patch (RFC 6902) sends an array of operations instead of a partial object. The intent is explicit: you are not saying "here is what the object looks like now," you are saying "apply this specific change." The `_links` object follows GitHub's approach, where clients can discover related resources without hardcoding URLs.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.