All articles
API DesignRESTBackendPHPWeb Development

REST API Design Rules I Wish Someone Had Told Me in Year One

PKParth Kanasagra
9 min read

The first API I built professionally had endpoints like /getUser, /updateUserInfo, and /do_delete_post. They all used POST. Errors came back as {"success": false, "msg": "something went wrong"}. There was no versioning. The mobile team sent me a four-page document of complaints after three months.

Here is what I know now after nine years of API work.

Name Resources, Not Actions

URLs should identify a resource (a noun). The HTTP verb tells you what you are doing to it:

// Bad — actions in the URL
POST /createPost
GET  /getPostById/5
POST /deletePost/5

// Good — resources + verbs
POST   /posts          → create
GET    /posts/5        → read
PUT    /posts/5        → replace
PATCH  /posts/5        → partial update
DELETE /posts/5        → delete

Use HTTP Status Codes Correctly

Stop returning 200 OK with an error in the body. The status code is part of the contract:

  • 201 — resource created (use on successful POST)
  • 204 — success with no body (use on DELETE)
  • 400 — bad request data
  • 401 — not authenticated
  • 403 — authenticated but not authorized
  • 422 — validation failed
  • 429 — rate limited

Consistent Error Responses Everywhere

{
    "message": "The given data was invalid.",
    "errors": {
        "email": ["The email field is required."],
        "name":  ["The name must be at most 255 characters."]
    }
}

Every error, from every endpoint, looking exactly like this. The mobile team should never have to write conditional error parsing logic.

Version From Day One

https://api.example.com/v1/posts
https://api.example.com/v2/posts

When v2 introduces a breaking change, v1 keeps working. Clients migrate on their schedule. I use URL versioning — header versioning is technically purer but harder to work with in practice.

Always Paginate Collections

{
    "data": [...],
    "meta": {
        "current_page": 1,
        "per_page": 20,
        "total": 4200,
        "last_page": 210
    },
    "links": {
        "first": "/api/v1/posts?page=1",
        "next":  "/api/v1/posts?page=2",
        "last":  "/api/v1/posts?page=210"
    }
}

An endpoint that returns an unbounded collection is a slow-motion disaster. The table has 10 rows today. It will have 10 million in two years.

Response Envelope Consistency

// GET /posts — collection
{ "data": [{...}, {...}], "meta": {...} }

// GET /posts/5 — single item
{ "data": {...} }

// Never mix naked objects with enveloped ones

Use Query Parameters for Filtering and Sorting

GET /posts?status=published&author_id=5&sort=published_at&order=desc&q=laravel

Do not invent new endpoints for filtered views. One resource endpoint, composable query params.

Related Articles

PK

Parth Kanasagra

Certified Senior Laravel & PHP Developer · 9+ years experience · Available for hire