Quick Answer

Name resources as plural nouns and use HTTP methods for the verbs. Return meaningful status codes rather than 200 with an error inside. Keep error responses consistent and machine-readable. Paginate any list that can grow. Validate every input at the boundary. Version the API before you need to break it, and never change existing behaviour without a new version.

Resource Naming and Methods

The core REST idea is that URLs identify things and HTTP methods describe the action. Putting verbs in URLs is the most common departure from that.

BAD                              GOOD
GET  /getUsers                   GET    /users
GET  /getUserById/5              GET    /users/5
POST /createUser                 POST   /users
POST /updateUser/5               PUT    /users/5
POST /deleteUser/5               DELETE /users/5

Use plural nouns consistently. /users and /users/5, not /user for one and /users for many. Mixing them forces consumers to remember which is which.

Nest for genuine relationships, but not deeply:

GET /users/5/orders           good — orders belonging to user 5
GET /users/5/orders/12/items/3/reviews    too deep — use /reviews?itemId=3

Two levels is usually the limit before URLs become unwieldy.

Use query parameters for filtering, sorting and pagination, not new endpoints:

GET /users?role=admin&sort=-created&page=2&limit=20

Know which methods promise what. GET must be safe — never change data in a GET, because browsers, caches and crawlers will call it. PUT and DELETE should be idempotent: calling them twice has the same effect as once. POST is not, which is why double-submitting a form can create two records.

Status Codes and Errors

Return the status code that matches what happened. The most common API mistake is returning 200 with an error in the body:

HTTP/1.1 200 OK
{ "success": false, "error": "User not found" }        ← wrong

Every layer that reads status codes — monitoring, retry logic, caches, client libraries — now believes this succeeded. Failures become invisible and errors get cached.

The ones you need:

  • 200 OK, 201 Created (include a Location header), 204 No Content for a successful DELETE.
  • 400 malformed request, 401 not authenticated, 403 authenticated but not permitted, 404 not found, 409 conflict such as a duplicate, 422 validation failed, 429 rate limited.
  • 500 your bug, 503 temporarily unavailable.

401 versus 403 is the pair most often confused: 401 means log in, 403 means logging in will not help.

Make errors consistent and machine-readable, with the same shape everywhere:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request contains invalid fields",
    "details": [
      { "field": "email", "issue": "must be a valid email address" },
      { "field": "age",   "issue": "must be at least 18" }
    ]
  }
}

A stable code lets clients branch on the error type without parsing English. Return all validation failures at once rather than one at a time, so a form can show every problem in a single pass.

Pagination and Response Shape

Paginate any list that can grow. An endpoint returning every record works with fifty rows in development and fails with fifty thousand in production.

GET /users?page=2&limit=20

{
  "data": [ ... ],
  "pagination": {
    "page": 2, "limit": 20, "total": 4137, "totalPages": 207
  }
}

Always cap the limit server-side. Without it, ?limit=1000000 is a denial-of-service vector any visitor can trigger.

Offset pagination is simple but has a real flaw: if rows are inserted while a user pages through, items shift and can be skipped or repeated. Cursor pagination — passing the last seen id — avoids this and performs better on large tables, at the cost of not being able to jump to an arbitrary page.

Be consistent about the envelope. Either every response wraps data in a data key or none do. Mixing them forces consumers to special-case endpoints.

Use consistent field naming — pick snake_case or camelCase and apply it everywhere, including nested objects.

Use ISO 8601 for dates in UTC2026-08-06T14:30:00Z. Never return a locale-formatted string or an ambiguous 06/08/2026, which means different days in different countries.

Never return sensitive fields. Password hashes, internal flags and tokens must be stripped before serialising, ideally by selecting fields explicitly rather than deleting them from the model.

Validation and Security

Validate every input at the boundary. Anything from a client is untrusted, including from your own frontend — a browser is not a trusted environment.

Check types, required fields, ranges, formats and lengths. Reject unknown fields rather than ignoring them, which catches typos in client code early.

Never use the request body directly. Mass assignment is a real vulnerability:

// Dangerous — a client can set { "role": "admin" }
await User.create({ ...req.body });

// Safe — take only what you expect
const { name, email } = req.body;
await User.create({ name, email, role: 'user' });

Authorise on every request, not just in the UI. Hiding a delete button is not security. Check on the server that this user may act on this resource — broken access control is consistently among the most common real-world vulnerabilities.

Rate limit public and authentication endpoints, and return 429 with a Retry-After header so well-behaved clients back off rather than hammering.

Use parameterised queries everywhere. String concatenation into SQL is how injection happens, and it has appeared in every edition of the OWASP Top Ten.

Do not leak internals in errors. A stack trace tells an attacker your framework, versions and file paths. Return a generic message with a reference id and log the detail against that id.

Versioning and Documentation

Version from the start. Adding a version later is far harder than including one from day one:

/api/v1/users

What counts as a breaking change: removing or renaming a field, changing a type, adding a required parameter, or changing what a status code means. Adding an optional field or a new endpoint is not breaking, which is why clients should ignore unknown fields rather than failing on them.

When you must break something, publish v2 and keep v1 running for a stated period. Silently changing behaviour breaks every consumer at once, and they will discover it in production.

Document it. An undocumented API is unusable by anyone who did not write it. OpenAPI generates browsable documentation and client code from a specification, and generating it from your route definitions keeps it from drifting out of date — which hand-written documentation always does.

Include working examples. A curl command a developer can paste and run is worth more than a table of parameter names.

The test for a good API: can a developer who has never seen it make a successful request within five minutes, using only the documentation? If the answer is no, the problem is usually inconsistency — endpoints that behave differently from each other for no visible reason.

Frequently Asked Questions

Should I use PUT or PATCH for updates? PUT replaces the whole resource and is idempotent, so omitted fields should be cleared. PATCH applies a partial update. Most APIs use PATCH for partial edits, and mixing them inconsistently is what confuses consumers.
Is it okay to return 200 with an error message? No. Monitoring, retry logic, caches and client libraries all read the status code, so an error returned as 200 is invisible to every one of them and may be cached as a successful response.
How should I version an API? Put the version in the URL path, such as /api/v1/users, from the start. Adding versioning later is much harder. Only break compatibility with a new version, and keep the old one running for a stated period.
What is the difference between 401 and 403? 401 means you have not proved who you are, so authenticating could fix it. 403 means the server knows who you are and is refusing anyway, so logging in again will not help.
Should every list endpoint be paginated? Any list that can grow, yes — and cap the limit server-side. An endpoint returning everything works in development with fifty rows and fails in production with fifty thousand, and an uncapped limit parameter is a denial-of-service vector.