Nahid Akbar

RESTful API Design Masterclass

RESTful APIs or REST for short stands for Representational State Transfer. "Representational" is the key phrase here.

REST came out of Roy Fielding's PHD dissertation in 2000 titled "Architectural Styles and the Design of Network-based Software Architectures". You can read the original dissertation here.

If you read it now, it will be full of duh's and wtf's. But having read it close to when it was published, it was amazing.

To summarise, this is what makes APIs "RESTful":

  1. It has a client-server request-response architecture.
  2. The API/Server is stateless. Requests are independent.
  3. Things are organised as resources that can be identified using URIs.
  4. Resources are manipulated through its representation.
  5. Responses are self-descriptive.
  6. Resource representation is unified and linked using HATEOAS.

There is a general theme of keeping server/API side as simple and scalable as possible and reducing coupling.

The dissertation is very vague on how to actually design REST APIs. It talks more about principles and constraints. A lot of the practical details came later in books and discussions in blogs and forums.

Why is it important?

When I was a junior developer in the 2000s, there was a lot of hype around REST from developers and managers. I feel that a lot of that has died down over the years but it is still a fundamental concept in API design. It's important to highlight what REST added to status quo and what it didn't.

How to design REST APIs

Example Problem

Lets take the simplest problem imaginable: A TODO list app. We will have users in the platform. Each user can have a list of TODO tasks.

Step 1: Model things as resources

REST APIs expose a resource management abstraction for whatever the API does. This is non-negotiable. As things have to be organised by resources, a good place to start is to identify the top level "resources" and their URIs. You might be used to thinking about resources as "classes" or "tables".

I've found it useful to think about three things to keep things grounded when designing anything:

First, who is this for?

For example, if the API is for consumer users, it makes very little sense to give them access to user management. However, if this API was available for the platform admins, then it would make sense to have user management.

Let's assume this API is for the users.

Second, what is needed from it?

For example, users will need to create, update, delete and view their tasks. They might need to mark tasks as complete or change due dates.

Third, how might this change in the future?

For example, users might want to manage multiple lists, share lists with other users. Business might be talking about having teams and organisations in the platform who have shared lists.

Just as all these things will inform how you design your services and classes and data models, they should inform the design of your API.

Tip: One useful strategy I like using when a project has multiple phases is to design the API for ideal world scenario and then trim out things that are not needed for this phase.

This will help you:

Based on above, we can identify one main resource for the first phase of the project:

Tasks. We will use the /tasks URI to represent this resource.

An example of over-engineering would be introducing a separate resource for "Status" for Tasks (e.g. /status/pending/tasks to serve a list of all pending tasks as nested resource). Just because something is "cool" or useful, it does not mean it is necessary. If you recall, one of our goal is to keep things simple. A more common approach is to query tasks by status is using query parameters (e.g. /tasks?status=done).

Tip: Another way to think about it might be an UML composition relationship. Task status isn't meaningful without a task. For example, if I made a new task status called "unicorn", how useful would it be if we never put tasks into that status? But tasks can exist fine without that status.

Having said that, there is no hard and fast rule. This part of design can be very subjective and can be the source of a lot of bike-shedding and conflict. If you have really good reason to have a resource like this, go for it.

Naming Rules

Nested resources

Nesting is when one resource is a sub-resource of another resource in the URI hierarchy. For example, /resources/{resourceId}/nested-resources.

Identifiers

Step 2: Pick resource format / media type

This refers to the format of the data that is sent and received from the API.

For most people, this is a no-brainer and does not warrant much thinking. They'll pick something they can most easily work with in their chosen tech stack.

Having said that, JSON is the most common format used for REST APIs these days. This stems from the fact that JSON is readily parsed in JavaScript which is used most commonly to consume these APIs. It is also the simplest of all the formats out there with the least number of features (e.g. no comments, no attributes, no references, no multiple ways of expressing the same thing, no DOM). Once you start using JSON, you rarely go back. YAML and XML are also used but are more feature complex and relatively less common.

Standard rules/features of HTTP request/response content processing apply here. For example:

Consistency is also desirable. Don't use xml for one operation and json for another.

Tip: Keep things simple and standard as described above. The idea here is, if you are designing an API for consumption in WEB for example, you should be able to readily use standard DOM APIs to use it. Most common way web API will be consumed is using fetch API or XMLHttpRequest in browsers. If you use standard conventions, they will be able to consume your API and parse your resource format into data without any special handling or post-processing.

Some conventions will ask for special media types (e.g. application/vnd.api+json for JSON:API). I personally don't see the value in breaking compatibility for this. If you are using standard JSON, use application/json.

Extra: Content negotiation

Content negotiation means allowing clients to request different media type or representations for the same resource. E.g. want JSON or XML etc.

Resource format negotiation

This is overrated in my opinion. For me, it goes against the principle of keeping server/API simple. However, if you were to implement it, the following two ways are most common:

First is to add an Accept header to the request stating the desired response format. For example, Accept: application/json or Accept: application/xml.

Second is to use file extensions in the URI. For example, /tasks.json or /tasks.xml.

The server then responds with the requested format if it supports it. If not, it can respond with a 406 Not Acceptable status code.

Version negotiation

Version negotiation is the process of agreeing on the version of the API that will be used for a particular request. This is important because APIs evolve over time, and clients need a way to specify which version they want to use.

There are several ways to implement version negotiation:

  1. URI Versioning: Include the version number in the URI. For example, /v1/tasks or /v2/tasks. This is most common. Some people use semantic versioning here as well which I think is overkill.
  2. Header Versioning: Include a custom header in the request that specifies the desired version. For example, X-API-Version: 1.0.
  3. Query Parameter Versioning: Include a query parameter in the request that specifies the desired version. For example, /tasks?version=1.0.

Resource content negotiation

This means client can request different representations of the same resource.

For example:

JSON:API for example can have the ?include=attachments query parameter to request linked resources.

Extra: Version Deprecation

When you design an API for others to use, it is important to have a plan for deprecating old versions. You don't want to be maintaining multiple versions of your API forever. It's another form of technical debt. That will slow down your development speed over time.

For any public facing API I design, I like to nest the actual API response data into a data field and have an optional deprecated field beside it. This is to indicate if the version is deprecated or not.

I make it consumers responsibility to raise alerts if they see any content in the deprecated field.

I also make it very clear that any deprecated version will be maintained for a maximum of X months (typically 6 months). I usually don't go out of my way to remove old compatibility layers but after a while, when adding and changing features, a natural breakage point comes. That's when I remove deprecated versions that are past expiry.

If your business has direct relationship with your API consumers, it is always a good idea to contact them separately as soon as a version is deprecated to give them heads up.

Design So far

The design so far is:

# Resource format: JSON

# Top level resources:
paths:
  # Task Management
  /tasks:

Step 3: Identify and map operations

What do we need to be able to do with our resources?

Common operations in TODO lists are:

  1. Create a task
  2. View a task
    1. Get a specific task
    2. Get a list of tasks
  3. Update tasks
    1. Change task order
    2. Change task status
    3. Change task name or description
    4. Assign due date
    5. Assign priority
    6. Add attachments to tasks
    7. Delete an attachment
  4. Delete a task

Map operations to HTTP methods

For REST APIs, we use HTTP methods to represent operations on resources. There is a long list of common and not so common HTTP methods. Some frameworks will allow you to make up any HTTP method you want. Personally, I have never needed to use much more than the 4 main CRUD methods when designing a REST API:

So, whenever we want to create something, we use POST, whenever we want to get some data, we use GET etc.

For our tasks, we can do the following:

paths:
  /tasks:
    post:
      summary: Create a new task (1)
      operationId: createTask
    get:
      summary: Get a list of tasks (2.1)
      operationId: listTasks
  /tasks/{taskId}:
    get:
      summary: Get a specific task (2.2)
      operationId: getTask
    put:
      summary: Update a specific task (3.1-3.5)
      operationId: updateTask
    delete:
      summary: Delete a specific task (4)
      operationId: deleteTask
  /tasks/{taskId}/attachments:
    post:
      summary: Add an attachment to a specific task (3.6)
      operationId: addAttachment
  /tasks/{taskId}/attachments/{attachmentId}:
    delete:
      summary: Delete an attachment from a specific task (3.7)
      operationId: deleteAttachment

You can see clear distinction between operations at top level resource level at /tasks and specific task level at /tasks/{taskId} identified by task id in the url.

I've made attachments a nested resource. Speaking from experience, representing potentially large attachment content (e.g. a word document) as Task attribute in JSON will be inefficient and cumbersome.

There are trade-offs to be made in all design. For example, this API does not have an operation to update an attachment making them immutable. This means the API client will need to add a new attachment and delete the old one to update an attachment. We essentially traded off having to build and maintain one less endpoint for little bit of extra work for the client - assuming this is something they need to do.

Do I need PATCH?

Some devs like to use PATCH for partial updates and they reserve PUT for full updates. I have never had a good reason to insist on this split.

What's stopping you from using PUT and updating all fields that need update? And if your resource representation is way too big, why not simply DELETE the resource and POST a new one?

You don't need to add additional complexity to save a few bytes of data transfer. That's not a good trade off for most use cases these days. For those use cases where you need to look at saving every byte, REST API's are a terrible choice anyway. So I think it represents intellectual laziness - people following trends without thinking for themselves or understanding the trade-offs.

Cross Origin APIs

The only other method you might need is the OPTIONS method.

If your API is to be consumed from browsers and they need to make requests cross origin, you will need to implement CORS (Cross-Origin Resource Sharing) headers to allow requests from different origins. This is a browser security feature enforced on the consumer side - I.e. it provides no protection outside browsers. So don't rely on it for security. But our consumers from browsers will need it.

This means adding a few headers to your API responses.

First is the Access-Control-Allow-Origin header. Here you can whitelist origins that can access your API. This will be good enough for the read only stuff (GET requests).

For write operations, you will need to implement OPTIONS method to support CORS preflight requests. Prior to making a write request, browsers will make an OPTIONS request to the same endpoint to check if the actual request is allowed. In those instances, if you want to allow write, respond with a Access-Control-Allow-Methods header listing the allowed methods.

In both cases, you will also need to respond with Access-Control-Allow-Headers header listing the allowed headers (e.g. Content-Type for them to interpret any response data).

What about other methods?

Generally, No.

For example, what if we need to search tasks? Should we use SEARCH method?

I would not. Instead, I would recommend using GET /tasks?query= with query parameters to filter the results. Without query parameters, it will return all tasks. With query parameters, it will return filtered results.

You might argue here that those are different operations. When we call 'GET /tasks', we are getting a list of all tasks. It's a database query operation. When we call 'GET /tasks?query=', we are searching and filtering tasks. We might be using a special search engine or service for this. Well, that's an implementation detail. Why would the consumers of the API need to know or care about that?

Using unique methods for every operation is also not going to scale. There aren't that many common HTTP methods. It's also going to be hard to find a method that meaningfully describes all your operations.

This is also one of my biggest pet peeves with REST APIs. For example, why is my update method called PUT and not UPDATE which is even more self-descriptive and makes more sense. You are adding a layer of obfuscation already to play the "REST API" game. Let's not make it worse and force your consumers to remember more meaningless translations.

What about non-CRUD operations?

This is where most people's API designs go wrong.

My advise is to lean on representation as much as possible.

For example, what if we need to mark a task as done. This could be PUT /tasks/{taskId} with {"status": "done"} in the request body.

Do not do POST /tasks/{taskId}/complete or something else - that's not RESTful. I recently saw an API labelled as REST from huge multinational company that had PUT /resources/{resourceId}/delete for deleting a resource. This is clearly not RESTful.

If you can't lean on existing representation, you have to expand representation. For example, imagine a feature where we want to introduce an endpoint to predict our chances of completing a task based on our past performance.

It's very tempting to do a remote-procedure style endpoint like POST /tasks/{taskId}/predict-completion, which is not RESTful.

In this case, we can change the 'predict-completion' verb to a noun and make it a resource. We could for example, do:

The difference is very small and subtle but it's the "representational" part of REST. It is the RESTful way.

Step 4: Pick resource representation

The next thing we need to do is to decide our resource representation. We should be able to derive this from our application data model which will be beyond the scope of this post.

Let's imagine our resource model looks like this:

Tasks Resource Model

Once we have some understanding of our resource model, we can design what the inputs and outputs of our API operations will look like.

We do this by thinking about what data is needed for an operation. Just because something is in our data model, it does not mean that it should be exposed in the API and vice versa. And the data format need not match exactly between what is happening inside our application and what API consumers see and deal with. We need to be thinking about use cases, security, privacy, and performance among other concerns.

I recommend keeping this as simple as possible. You can always add complexity later. My general thinking is, unless you change an existing field or remove it, your subsequent additions are backward compatible. Unless the client has done something really stupid like added a validation that assumes new fields won't be added, they should be fine. If you really need to make breaking changes, it is best to introduce a new version of the API so the older version can be deprecated and kept for a little while.

Example: POST /tasks

To create a task, the API needs to know the following fields:

This is the only required field we can't default. For example, for a new task, we can default:

Hence, our Create Task operation representation can look like this:

paths:
  /tasks:
    post:
      summary: Create a new task
      operationId: createTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  description: The title of the task
              required:
                - title
      responses:
        '201':
          description: Task created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: The unique identifier for the task
                  title:
                    type: string
                    description: The title of the task
                  description:
                    type: string
                    description: The description of the task
                  status:
                    type: string
                    description: The status of the task (e.g., pending, completed)
                  dueDate:
                    type: string
                    format: date-time
                    description: The due date of the task
                  priority:
                    type: string
                    description: The priority of the task (e.g., low, normal, high)
                  attachments:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: The unique identifier for the attachment
                        fileName:
                          type: string
                          description: The name of the attachment file
                        url:
                          type: string
                          description: The URL to access the attachment
                    description: List of attachments associated with the task
                  createdAt:
                    type: string
                    format: date-time
                    description: The timestamp when the task was created
                required:
                  - id
                  - title
                  - status
                  - priority
                  - attachments
                  - createdAt

Tip: Above syntax is YAML representation of the JSON schema for the request and response. High level wrapping (e.g. paths: /path method: etc) is the OpenAPI specification (formally known as swagger) which is the de-facto standard for describing REST APIs in the software industry. So if you are going to be designing REST APIs, I becoming familiar with both intimately.

You can find the full specification I've produced here. You can copy and paste that into Swagger Editor to see a nice visual editor and representation of the API.

It's common for APIs to serve this document in a /swagger.json or /openapi.json endpoint. It is also common to serve swagger-ui in '/docs' or '/api-docs' URI to provide interactive API documentation.

In the ideal world scenario, your web framework will have annotation libraries to generate this documentation from your API router code automatically so you don't have to maintain it separately. It has been the case for almost all web frameworks I've used in the last 15 years.

Reality check: Is this API RESTful?

Let's go through our checklist:

  1. It has a client-server request-response architecture. ✅ No evidence to the contrary.
  2. The API/Server is stateless. Requests are independent. ✅ No evidence to the contrary. For example, I once worked on an API that required the API server to HTTP POST result of request back to the client. This was clearly not stateless.
  3. Things are organised as resources that can be identified using URIs. ✅ Our resources comply: /tasks/{taskId}, /tasks/{taskId}/attachments/{attachmentId}.
  4. Resources are manipulated through its representation. ✅ All our operations manipulate resources through their representation.
  5. Responses are self-descriptive. ✅ Requests will respond with appropriate Content-Type headers. We will also using standard HTTP status codes to describe the result of operations.
  6. Resource representation is unified and linked using HATEOAS. ❌ - We are not using HATEOAS. I don't think it's necessary for most APIs.

HATEOAS - Hypermedia As The Engine Of Application State

HATEOAS says that a client should be able to use the API entirely through hypermedia provided dynamically by the server. This means API can be even more self-descriptive and client need to have less prior knowledge about how to use the API.

For our example, this means our resource representation should return links to all possible resources that are related to that task.

For example, a GET /tasks/{taskId} response might look like this:

{
  "id": "{taskId}",
  "title": "Buy groceries",
  "description": "Milk, Bread, Eggs",
  "status": "pending",
  "dueDate": "2025-10-26T12:00:00Z",
  "priority": "normal",
  "attachments": [{
    "id": "{attachmentId}",
    "fileName": "fridge-image.png",
    "links": {
      "self": "/tasks/{taskId}/attachments/{attachmentId}"
    },
  }],
  "createdAt": "2025-09-26T10:00:00Z",
  "links": {
    "self": {
      "href": "/tasks/{taskId}"
    }
  }
}

However, it remains a not very well thought out dream. Returning a bunch of links with response is simple enough. The problem with this is:

The result is complete chaos with no client libraries or frameworks that support using these links.

More recent standards try to fill this gap. For example, these days, you can use HAL (Hypertext Application Language) or JSON:API standards to represent links in your resource representation.

They are good starting points but still are very short sighted. They do very little to solve the underlying problem which are:

But APIs have to have HATEOAS to be RESTful, right? I guess you will have to add some "hyperlinks" to satisfy the imaginary or your colleagues turned into self appointed REST API police. ¯\_(ツ)_/¯

Richardson Maturity Model

The Richardson Maturity Model describes the maturity of a REST API. It has four levels going from not so RESTful to fully RESTful.

These are:

Level 0 - The Swamp of POX

This can take many shapes:

Level 1 - Resources

At this level, resources are identified using URIs.

Maybe something like:

Method URI Description
POST /tasks-create Create a new task
POST /tasks Get list of tasks
POST /tasks-get-by-id Get a specific task
POST /tasks-delete Delete a specific task

Level 2 - HTTP Methods

At this level, HTTP methods are used to represent operations on resources.

Method URI Description
POST /tasks Create a new task
GET /tasks Get list of tasks
GET /tasks/{taskId} Get a specific task
DELETE /tasks/{taskId} Delete a specific task

Level 3 - HATEOAS

At this level, HATEOAS is used to provide hypermedia links in the resource representation.

Written September 2025
© Nahid Akbar