The Complete Guide to RESTful API Design — 10 Principles + Real-World Examples
Last updated: June 23, 2026 · 12 min read · Category: Backend Engineering
📌 Key Takeaways
- The Complete Guide to RESTful API Design — 10 Prin is widely used by developers
- Based on RFC standards and real-world experience
- Free online tools, runs locally, no data upload
- FAQ section at the bottom answers common questions
RESTful API design is the cornerstone of every modern backend system. Whether you're building a small service or a public-facing platform, following a consistent set of RESTful best practices means fewer surprises, faster onboarding, and APIs that age well. This guide distills the 10 core principles, walks through a real Node.js/Express example, lists the 10 most common anti-patterns, and ends with an in-depth REST vs GraphQL comparison.
1. What Is REST? Why Does Design Quality Matter?
REST (Representational State Transfer) was proposed by Roy Fielding in his 2000 doctoral dissertation. Its core idea is simple: treat the URL as a resource identifier, and use HTTP verbs to express actions on that resource. The data format is usually JSON, and every resource is uniquely addressable.
Why does RESTful API design quality matter so much? Three reasons:
- Decoupling: Frontend and backend teams can iterate independently as long as the contract is stable.
- Longevity: A well-designed API can serve the business for years; a poorly designed one will require painful rewrites within months.
- Ecosystem: REST plays well with CDN caching, API gateways, monitoring, documentation generation (OpenAPI), and the entire tooling chain.
Before diving into principles, let's lock in the API design principles we'll be using throughout. They are also the reason this article can serve as a permanent reference for your team.
2. The 10 RESTful Best Practices
Principle 1: URLs are resources, verbs live in HTTP methods
The most common mistake beginners make is putting verbs in the URL:
GET /users/123 # Fetch user with id=123
POST /users # Create a user
PUT /users/123 # Update user with id=123
DELETE /users/123 # Delete user with id=123
Once you move verbs from the URL into HTTP methods, your API becomes self-describing — anyone can read the intent from the URL + Method without checking the docs.
Principle 2: Use plural resource names — keep collections and items consistent
Resource collections should use plural nouns, so that "lists" and "single items" stay stylistically consistent in URLs. Even if a resource is logically always a singleton (such as /settings), it's still recommended to keep it plural so future extensions don't force a breaking rename.
❌ GET /user/123 → ✅ GET /users/123
❌ GET /article/5 → ✅ GET /articles/5
Plural naming also makes it easy to add batch operations (POST /users/batch-delete) and aggregated resources (GET /users/active) later.
Principle 3: Express intent with HTTP verbs — 5 methods cover CRUD
RESTful API design principles require strict use of HTTP verbs to express semantics — this is the heart of the uniform interface. The semantics of the 5 common verbs:
- GET: Idempotent and safe (does not change server state); used to read a resource.
- POST: Non-idempotent; used to create a resource or trigger a non-idempotent action (e.g. login, place an order).
- PUT: Idempotent; used to fully replace a resource (the client must send every field).
- PATCH: Can be used for partial updates (change only what you send) — ideal for "incremental update" scenarios.
- DELETE: Idempotent; used to delete a resource.
Example:
GET /articles # List
GET /articles/42 # Detail
POST /articles # Create
PUT /articles/42 # Full replace
PATCH /articles/42 # Partial update
DELETE /articles/42 # Delete
Principle 4: Use HTTP status codes precisely — never "all 200"
HTTP status codes are the first signal a client uses to judge a request's outcome. Stuffing business errors into a 200 is the most common anti-pattern — it forces the frontend to parse the body just to find out whether the request succeeded. Use them as follows:
- 2xx Success: 200 OK (general success), 201 Created (resource created), 204 No Content (delete / success with no body).
- 3xx Redirection: 304 Not Modified (cache hit), 301/302 (resource location changed).
- 4xx Client Error: 400 Bad Request (bad parameters), 401 Unauthorized (not authenticated), 403 Forbidden (authenticated but no permission), 404 Not Found, 409 Conflict (resource conflict), 422 Unprocessable Entity (semantic parameter error), 429 Too Many Requests (rate limited).
- 5xx Server Error: 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout.
Using the right status code is the most overlooked yet highest-leverage step in API design conventions.
Principle 5: Versioning — leave room for evolution from day one
APIs will change, but already-published endpoints should never be silently broken. The most robust engineering practice is to put the version number in the URL path (e.g. /api/v1/users) rather than burying it in a header or query string — the path is friendliest to humans, to CDN/gateway routing, and to search engines.
Comparison of three mainstream versioning approaches:
- URI Path (recommended):
/api/v1/users— intuitive and easy to route. - Query Parameter:
/api/users?version=1— doesn't break RESTful style but is harder for gateways to forward. - Header Media Type:
Accept: application/vnd.myapi.v1+json— the most "RESTful" but unfriendly to frontend developers.
A new version should run in parallel for at least 6–12 months, giving clients a comfortable migration window.
Principle 6: Use JSON, not XML — but keep Content-Type negotiation
As of 2026, JSON is the de facto standard: small payload, highly readable, natively compatible with JavaScript. Unless your client requires XML (such as certain financial or government systems), always use Content-Type: application/json; charset=utf-8.
Response example:
{
"id": 123,
"name": "Alice",
"email": "[email protected]",
"created_at": "2026-06-08T10:00:00Z"
}
Always explicitly declare Content-Type in the response — don't make the client guess.
Principle 7: Filter, sort, paginate — standard for collection endpoints
Once a resource collection can grow large, you must support filtering, sorting, and pagination. Query strings are recommended:
GET /api/v1/orders?status=active&sort=-created_at&page=1&limit=20
GET /api/v1/orders?created_after=2026-01-01&search=phone
GET /api/v1/orders?fields=id,total,status # Sparse fieldsets
Three common pagination strategies:
- Offset pagination:
?page=1&limit=20— simple to implement, but performance degrades on deep pages. - Cursor pagination:
?cursor=***==&limit=20— stable performance, ideal for infinite scroll. - Keyset pagination:
?after_id=123&limit=20— combines with a sort column; the go-to choice for large datasets.
It's also recommended to include pagination metadata in the response body:
{
"data": [ ... ],
"meta": { "page": 1, "limit": 20, "total": 1024, "has_more": true }
}
Principle 8: Express nested resources with path hierarchy
When a resource naturally belongs to another, use path nesting to express the relationship:
GET /users/123/orders # All orders of user 123
GET /users/123/orders/456 # Order 456 of user 123
POST /users/123/orders # Create an order for user 123
GET /articles/42/comments # Comments on article 42
But note: nesting should not exceed 3 levels, otherwise the URL becomes hard to maintain. Beyond 3 levels, switch to a top-level resource with filtering:
GET /orders?user_id=123 # Better than /users/123/orders/recent/top-5
Principle 9: Unify error response format — make errors machine-readable
A good error response should be both human-readable and machine-readable. A unified structure is recommended:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with id=999 does not exist",
"details": {
"resource": "user",
"id": 999
},
"request_id": "req_8f3a2b"
}
}
Key takeaways:
- code: A machine-readable string enum; the frontend can
switchon it directly. - message: A human-readable localized string (return it per
Accept-Languagefor i18n). - details: Can hold field-level errors (validation), documentation links, etc.
- request_id: A server-side log tracing ID for one-click debugging.
Principle 10: Security — HTTPS, CORS, rate limiting, and authentication are all mandatory
The API is the "front door" of your system — security is non-negotiable. Four must-haves:
- HTTPS (TLS 1.2+): Every API must run over HTTPS; plain HTTP must be disabled. Let's Encrypt is the free de facto standard.
- CORS: Cross-origin requests must explicitly set
Access-Control-Allow-Origin. Don't blindly use*— for credentialed requests, scope it precisely to the domain. - Rate Limiting: Use a token bucket or sliding-window algorithm to cap QPS per IP / per user; return
429with aRetry-Afterheader. Redis + Lua is a popular implementation. - Authentication & Authorization:
- Prefer stateless JWT (OAuth 2.0 / OIDC) for APIs.
- Server-rendered apps can keep Session-Cookie.
- Add 2FA / OTP for high-risk operations.
- Enforce CSRF protection on every write endpoint.
3. Hands-On Example — A Todo API with Node.js/Express
Theory's done — time for code. Below is a production-grade, minimum-viable Todo API covering all 5 endpoints (GET/POST/PUT/PATCH/DELETE) plus JWT auth, a unified error format, and rate limiting. Just copy and run:
// app.js
import express from 'express';
import jwt from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';
const app = express();
app.use(express.json());
app.use(rateLimit({ windowMs: 60_000, max: 100 })); // 100 req/min/IP
const SECRET = process.env.JWT_SECRET || 'dev-secret-change-me';
let todos = [{ id: 1, title: 'Learn REST', done: false }];
let nextId = 2;
// ----- Unified error middleware -----
const errorHandler = (err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.message || 'Server Error',
request_id: req.id
}
});
};
// ----- JWT auth middleware -----
const auth = (req, res, next) => {
const token = (req.headers.authorization || '').replace('Bearer ', '');
try { req.user = jwt.verify(token, SECRET); next(); }
catch { next({ status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }); }
};
// ----- 5 RESTful endpoints -----
app.get ('/api/v1/todos', auth, (req, res) => res.json({ data: todos }));
app.get ('/api/v1/todos/:id', auth, (req, res) => {
const t = todos.find(x => x.id === +req.params.id);
if (!t) return next({ status: 404, code: 'TODO_NOT_FOUND', message: 'Todo not found' });
res.json({ data: t });
});
app.post ('/api/v1/todos', auth, (req, res) => {
const t = { id: nextId++, title: req.body.title, done: false };
todos.push(t);
res.status(201).json({ data: t });
});
app.put ('/api/v1/todos/:id', auth, (req, res) => {
const t = todos.find(x => x.id === +req.params.id);
if (!t) return next({ status: 404, code: 'TODO_NOT_FOUND', message: 'Todo not found' });
Object.assign(t, req.body); // Full replace
res.json({ data: t });
});
app.patch ('/api/v1/todos/:id', auth, (req, res) => {
const t = todos.find(x => x.id === +req.params.id);
if (!t) return next({ status: 404, code: 'TODO_NOT_FOUND', message: 'Todo not found' });
Object.assign(t, req.body); // Partial update
res.json({ data: t });
});
app.delete('/api/v1/todos/:id', auth, (req, res) => {
todos = todos.filter(x => x.id !== +req.params.id);
res.status(204).end();
});
app.use(errorHandler);
app.listen(3000);
Test requests (using curl):
# Log in to get a token
curl -X POST http://localhost:3000/login -d '{"user":"alice","pass":"x"}' -H 'Content-Type: application/json'
# Create a todo
curl -X POST http://localhost:3000/api/v1/todos -H "Authorization: Bearer *** -H 'Content-Type: application/json' -d '{"title":"Finish the RESTful tutorial"}'
# Partial update (PATCH)
curl -X PATCH http://localhost:3000/api/v1/todos/1 -H "Authorization: Bearer *** -H 'Content-Type: application/json' -d '{"done":true}'
In roughly 60 lines of code you've fully covered the 10 core principles of RESTful API design.
4. 10 Most Common RESTful Anti-Patterns
With the good practices covered, let's call out the bad ones. These 10 errors appear most often in code reviews:
- Verbs in the URL:
/getUsers,/createOrder— violates "the URL is a resource, not an action." - Returning 200 for everything: Stuffing business errors into
{ success: false, code: 1 }forces the frontend to parse the body. - Not distinguishing POST / PUT / PATCH: Using POST for create, full update, and partial update breaks idempotency.
- No versioning: Shipping breaking field changes on
/api/usersovernight brings clients down with you. - No pagination:
GET /usersreturns 100k rows at once — an OOM is only a matter of time. - Inconsistent error shapes: Sometimes
msg, sometimesmessage, sometimeserror— the frontend ends up writing a wall of if-else. - Ignoring HTTP caching: GET endpoints don't return
ETag/Cache-Control, wasting CDN capacity for nothing. - Mixed field-naming conventions:
userId,user_id,userIDall in the same API — pick camelCase or snake_case and stick with it project-wide. - Mapping every error to 500: Bad parameters, missing permissions, missing resources all return 500 — the logs turn into a sea of red and the real problem gets buried.
- Plain HTTP + no authentication: Production still runs on
http://api.xxx.com, the login endpoint has no HTTPS, tokens travel in the clear.
Avoid these 10 pitfalls and your API will be solid and professional in 95% of scenarios.
5. RESTful vs GraphQL — How to Choose?
Discussing the REST vs GraphQL difference is almost a rite of passage in backend interviews. The two aren't about "one replacing the other" — they fit different scenarios:
- REST strengths: Simple, native HTTP, cache-friendly, low learning curve, mature tooling (Swagger/OpenAPI, Postman, curl). A great fit for public, open APIs, CRUD-heavy workloads, and scenarios that need CDN caching.
- GraphQL strengths: Single endpoint, the client asks for exactly the fields it needs, no need for multiple versions, strongly typed schema. A great fit for fast-changing frontends that aggregate multiple data sources and mobile clients on flaky networks.
A one-line rule of thumb: general-purpose, stable workloads, heavy caching → REST; many clients, heavy aggregation, fast iteration → GraphQL. Small and mid-sized teams should default to REST and reach for GraphQL only when the scenario demands it. The two can also coexist — many large systems use REST as the public gateway and GraphQL as the internal aggregation layer.
6. Conclusion
Mastering the 10 RESTful API design principles above covers 95% of the APIs you'll build in your career. Always remember: URL is the resource, verbs live in HTTP methods, status codes are the first signal, error format must be unified, and security is non-negotiable. A great API is one a new developer can understand within 5 minutes — that is the true measure of API design principles.
For quick lookups during your day-to-day work, try the related tools in our toolbox:
- JSON Formatter & Validator — Beautify and validate JSON responses in real time.
- JWT Decoder — Inspect token claims and signature algorithm.
- URL Encoder / Decoder — Safely encode query strings and path parameters.
- Base64 Encoder / Decoder — Decode Basic Auth headers and other base64 payloads.
- Regex Tester — Quickly verify route patterns and validation regex.