DEV Community

Cover image for Essential guide to designing an effective REST API
Prabesh Thapa
Prabesh Thapa

Posted on

Essential guide to designing an effective REST API

Table of contents

What is an API ?

API stands for Application Programming Interface. It is a way a program offers service to other programs. Think of it like a waiter. Chef and customer are your services and waiter is an API. Job of waiter or API is to provide data or food created by chef or one program to customer or another service.

What are the 3 horsemen of well designed API ?

The main traits of a well designed API are:

  1. Easy to read and work with
  2. Hard to misuse
  3. Complete and concise

Steps to design an effective API ?

Here are some of the ways you can make your API robust and scalable:

  • Decide what kind of API is it it will be. Whether it will be public, private or composite.

  • Identify resource and sub-resources. whether it is singleton or collection i.e customer and customers or collection and sub-collections i.e customers and accounts . Eg: take an example of a books api. Here there will be two resource at minimum. Books can have many book options

    Books: collection of books
    
    Book: individual book
    
  • Create URI based on those resource figured above.

    /v1/books
    /v1/books/{bookID}
    
  • Decide how you want to produce data. Generally accepted format is JSON format.

  • Assign HTTP Methods to the URI created above.

    HTTP GET /rooms
    HTTP GET /rooms/{roomId}
    
  • Use versioned API for scalability.

  • Do not use CRUD names in API endpoints

    Wrong:
    ENDPOINT/createUser
    ENDPOINT/deleteUser
    
    Correct:
    HTTP POST ENDPOINT/user/{userID}
    HTTP DELETE ENDPOINT /user/{userID}
    
  • Make API idempotent. API consumer will make mistake, they might write code in such a way that it results in duplicate request. Make sure API is idempotent.

    * `POST` is not idempotent. 
    
    * `GET, PUT, DELETE, HEAD, OPTIONS, TRACE` are idempotent operations
    
  • API exposed in response should be JSON encoded.

  • Should only support required HTTP Methods. All request other than allowed methods, should return 405 Method not allowed status code.

  • Use snake_case or camelCase for keys. Either is acceptable.

  • Know when to use PUT and when to use POST.

    * PUT is idempotent. Use PUT to update a single resource i.e PUT replaces the resources entirety. use PUT for update operations.
    
    * Use PATCH is only certain part of resource needs to be updated.
    
    * POST is not idempotent. Use it for create operations
    
  • Should provide appropriate json response back to the API caller. i.e 200-type response. Consider following things

    • Do not put redundant information in the api response.
  • For example, there is no point adding isSuccess: true or success: true or message: success.

    • You will know if the request was success or not via the HTTP status code. You can put anything in message or make typo. Standards are there for a reason, learn to embrace them.
    • The client application behaved erroneously (client error - 4xx response code)

      Response header:
      < HTTP/2 404
      < content-type: application/json; charset=utf-8
      < content-length: 59
      ...
      
      Response body:
      {
        "data": "null",
        "message": "listing not found.",
        "status": 404
      }
      
    • The API behaved erroneously (server error - 5xx response code)

        {
          "data": "null",
          "message": "Could not find the item.",
          "status": 500
        }
    
    • The client and API worked (success - 2xx response code)
        Response header:
        < HTTP/2 404
        < content-type: application/json; charset=utf-8
        < content-length: 59
        ...
    
        Response body:
        {
          "data": {
            ..REDACTED
            "created_at": "2022-11-10T10:25:21.438118-08:00"
          },
          "message": "created",
          "status": 200
        }
    
  • Should do one and only one thing. Each feature can have multiple services working together to make it happen meaning, log feature needs to have two service UI service, auth service to make it possible. This will make each service independently deployable and manageable.

  • Should have CORS implemented for your service. It's important to have the OPTIONS check since CORS check uses OPTIONS as part of the preflight check. If HTTP request OPTIONS failed, it would fail the CORS.

    Example cors headers:

    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Methods: GET, POST, 
    Access-Control-Allow-Headers: Content-Type,access-control-allow-origin, access-control-allow-headers
    Access-Control-Allow-Origin: * 
    
  • Do not use abbreviations. Build an API which developers will love not hate. API should be self explaining.

  • Use nouns and plural nouns as required.

  • No Verbs in the URL.

  • Be careful while nesting. At most two functions could be tested into an endpoint. If it is more than that then separate them into different endpoint. Be careful of the length of the API endpoint. Different browsers have different limit but we want to make sure application is available to all browsers hence if you are using GET then the length might exceed url length limit. While there is no specific limit defined in RFC, Standard length of an URl is 2000 chars ( IE limit ).

  • Should expose metrics via “/metrics” endpoint into TSDB format so that application like Prometheus can consume it for analysis and visualization.

  • Should allow filtering, sorting, field selection and paging.

  • If repeated information is fetched alot of time then, try implementing Caching ( in-memory ) ( eg: redis ). This will help improve api performance. Caching can be a double edge sword so you should be very careful when selecting candiadtes of caching.

  • Do not make API chatty. A chatty API has alot of issues among which the main is, logs from API will be sent to some platform. More the logs, more space is required to store and parse them. If request for similar content is asked then use dynamic sampling.

  • Should not handle request from non-desired services. According to RFC 7231 section 6.5.5 method not allowed should send back allowed methods to the user.

      < HTTP/2 405
      < Allow: GET
      < content-type: text/plain
      < content-length: 22
      < date: Thu, 10 Nov 2022 19:02:48 GMT
    
  • Should have readiness and liveliness endpoint (eg /live and /ready for health check. To understand this, first understand what is a healthy API or what do you consider an application to be in healthy state. At least make sure health check is exposed only after internal dependency testing is verified. You application cannot be called healthy if it runs but cannot fetch data from database.

% curl -s ENDPOINT/live

    {
      "code": 200,
      "message": "running"
    }
Enter fullscreen mode Exit fullscreen mode
    % curl -s ENDPOINT/ready

    {
      "code": 200,
      "message": {
        "status": {
          "database":"operational"
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode
  • Should log properly in a standard format format. Recommendation: JSON as lot of log parsing application aspect log messages in JSON format. There are two types of logging, application or business logic logging and transport logging. Both should be done

    {
      "method": "GET",
      "path": "/health",
      "remote_addr": "127.0.0.1",
      "response_time": "31.356µs",
      "start_time": "2022/11/10 - 10:42:38",
      "status_code": 200
    }
    
  • Add feature to identify reach request via requestID. This makes correlation easier when debugging.

  • Use essential HTTP headers ( at least use following )

    • Content-Type to specify what kind of data are you expecting and also what kind of data is being sent in response.
    • Content-Length to provide size of the body. This provides two benefits which is first, client can know it read correct number of bytes from connection and second is client can know the size of body without downloading it.
    • Last-Modified to know if something changed to the state of the resource. This is useful when used with caching.
  • Always sanitize and validate data coming in before proceeding with any operation. Do not assume it is done in frontend. Use Content-Security-Policy headers.

  • Make sure your api is protected against OWASP Top 10 attacks.

  • Protect critical endpoint using authentication and authorization. Recommendation use JWT or PASETO token. Use access and refresh token. Access token can be valid for around 15min to 1 hr depending on average session and refresh token can be 24 hr. Follow principle of least privilege for authorization.

  • Add DOS protection by implementing rate limiting. You can do so by throttling API calls. It depends on how much request are you expecting to see and use that as a soft limit. Once soft limit is found, you can add few request more and test thing out before implementing the hard limit.

  • Make sue API supports HTTPS natively or communication between service is secured using mTLS.

Conclusion

Considering above topics will help you build a good API. While this only considers the development side of things, you would need to make sure infrastructure, packaging and deployment is also done properly for your API to work effectively. I hope this information was useful for you. I have started to create API development in Go series where we will address all of the above mentioned topics and try to build an reliable API. Subscribe and follow along: Go bootcamp

Top comments (0)