REST APIs are one of the most common kinds of web interfaces available today, allowing various clients, mostly browser apps, to communicate with services via the REST API. The interface is meant to evolve with the service, ideally following some commonly accepted conventions, which helps the maintainers and the users of the API stay aligned [1]

This article will outline some best practices for building REST APIs, the annotations supported by Spring Boot for use in the JVM, a nd how these translate into OpenAPI specs. We’ll examine the same API outlined in this previous article, which outlines a pet store application where users can place orders.

What is a REST API?

REST stands for Representational State Transfer. APIs in this space conform to the following five design principles [2]:

  1. Uniform Interface: All API requests for the same resource (URI) should look the same, regardless of where the request comes from.
  2. Client-Server Decoupling: Client and server applications must be independent of one another. The only information the client should know is the URI of a requested resource. Similarly, the server shouldn’t modify the client application; it should only send it the requested data.
  3. Statelessness: REST APIs are stateless, meaning that each request needs to include all the information necessary for processing. Server-side sessions are a no.
  4. Cacheability: Ideally, resources should be cacheable on the client- or server-side. Typically this is indicated in response headers for a delivered resource.
  5. Layered System Architecture: Calls and responses can (and should!) go through different layers. APIs should be designed so that neither the client nor server can tell whether it’s communicating directly or via an intermediary.

Code on Demand

The optional sixth standard. This is the ability for the server to send back code for the client to execute.

Request Types

There are four main resource methods that are associated with REST APIs, though they are technically HTTP request methods [3]:

  1. GET: Ask the server to retrieve some piece of data e.g. “Fetch me the list of items in my cart.”
  2. POST: Ask the server to create a new piece of data e.g. “Add some dog treats to my cart.”
  3. PUT: Ask the server to update some piece of data e.g. “Update my order to contain two dog toys instead of one.”
  4. DELETE: Ask the server to delete some piece of data e.g. “Remove tennis balls from my cart.”

These core methods allow users to perform a variety of state-changing operations on the server. They are also sometimes referred to as CRUD Operations, an acronym for Create, Read, Update, and Delete.

Although REST calls are state-less, that doesn't mean they can't update the state on the server-side! The calls cannot act on previous calls implicitly.

e.g., “Remove the last item I added from my cart” relies on state from the previous call, which is non-conforming design.

Shape of a Request

Now that we know what types of REST requests are typically made, let’s look at the actual shape of the request itself. There are a few components to a REST call:

  • Server Host: The first part of the URL, indicating which server to make the request e.g. https://carsonboden.com
  • Uniform Resource Identifier (URI): The part of the URL which identifies which resource the client is attempting to access e.g. /inventory/items/123
  • Query Parameters: Typically only for GET requests, these are additional path parameters in the URL often used to sort, paginate, or filter the request e.g. ?zip=98115
  • Request Body: A payload sent with the request containing additional information, typically as a JSON blob. e.g. { item: "Dog Food", amount: "20lb bag" }

Here are some examples of full REST requests and what they might mean:


GET https://pet-store.com/stores?zip=98115 Fetch all stores from the petsore service that have a ZIP of 98115

POST https://pet-store.com/cart { item: "Dog Food", amount: "20lb bag" } Add a 20lb bag of dog food to my cart in the Pet Store application.

PUT https://pet-store.com/item/123/notes { content: "Lorem ipsum" } Update the notes for item 123 to be, “Lorem ipsum”


We'll see further down how Spring Boot annotations map to these operations. For example, @RequestParam is used to mark query parameters within a Controller.

Best Practices

We can see from the above examples that REST APIs are very flexible, which makes them a powerful tool for building out back-end services. But this flexibility can also make it easy to define endpoints that don’t evolve in a coherent way over time.

Using each Parameter Type

The URI, query parameters, and request body are all ways that we can pass information to the server, but these have implications on how the API will evolve.

For example, creating an endpoint like /items/filter/{manufacturerName} might enable callers to get a list of store items, filtered by the manufacturer name, but if we want to add another filter, how could we add it?

  • We can’t easily define another URI parameter like /items/filter/{zip}, since we can’t guarantee a distinction between ZIP vs. manufacturer name
  • We can’t easily filter by both manufacturer name and another parameter, since this filter endpoint only accepts the name

We should instead use a query parameter to filter requests, as those can be extended without breaking existing endpoints, as they're typically not required.

With query params, a call might be: /items?manufacturerName=FooBar&zip=98115

As a general rule of thumb, each parameter should be used as follows:

  • URI Parameters: Refer to a single resource (or collection of resources). It can be helpful to think of URIs as the “nouns” we act on via the “verbs” of our HTTP requests. For example, we can GET a collection of meters at /meters
  • Query Parameters: Used for filtering, sorting, or paginating requests. Don’t create a new endpoint, add query parameters to existing endpoints!
  • Request Body: Best for all other incoming parameters. Most of the time it’s more flexible to pass values in via a request body than passing it in via the URI.

Nesting Resources in a Hierarchy

If one object contains another object, it is often beneficial to define the endpoints to reflect that. This is good practice regardless of how the data is structured in the database.

For example, at the pet store, perhaps our items have comments associated with them. As such, we can nest a /notes endpoint at the end of our existing /items endpoint:


/items/{itemId} GETs high-level information about an item (e.g. location, whether it’s in stock)

/items/{itemId}/notes GETs the notes for that particular item. Might also support POST/PUT/DELETE

After the third level or so, nested endpoints can get unwieldy. Instead consider returning the URI to those resources, especially if that data isn't contained within the top-level object (which is more common for micro-services)

/items/{itemId}/notes/{noteId}/authors GETs the author of a note, and could return, e.g.: { author: "/users/{userId}" } rather than needing to nest: /items/{itemId}/notes/{noteId}/authors/{authorId}


Handling Errors

HTTP response codes are well-defined such that both the client and server can understand and relay the problems that have occurred. From the server-side, we don’t want errors to bring down our system, so we often return something to the client so they can handle it.

HTTP status codes do not always translate to an exception on the client-side. Instead, the client might retry the request or make it known to the user that they've input invalid data.

Some common HTTP status codes include:

  • 400 Bad Request: The client made a bad request to the server. Generally, any 4XX status code indicates a failure on the client’s part.
  • 403 Forbidden: The user is authenticated by does not have access to the resource.
  • 404 Not Found: The resource at the URI was not found. This is the JVM equivalent of returning null — don’t try to actually return null from a Controller!
  • 500 Internal Server Error: The server failed to process the request. Generally, any 5XX status code indicates a failure on the server’s part.
  • 502 Bad Gateway: There was an invalid response from an upstream server, e.g. for services that call out to other services.

There’s a long list of HTTP status codes, no need to memorize them all! Some are funny, like 418 I’m a Teapot, which was created for April Fool’s.

Annotating for Spring Boot

The web services library Spring Boot provides some great annotations in the JVM for easily defining REST Controllers which can service these requests. The annotations let us build functions which will automatically read incoming values from the request URI, query parameters, or request body:

  • @GetMapping, @PostMapping, etc.: Method-level annotations for which type of HTTP request they should serve. They are each extensions of @RequestMapping, which typically should be replaced with the method-specific annotation.
  • @RequestBody: Parameter-level annotation indicating that the data should be sent via the request body.
  • @RequestParam: Parameter-level annotation indicating that the data should be sent via the query parameters.
  • @PathVariable: Parameter-level annotation indicating that the data should be sent as part of the URI.

Here’s an example of a Controller:

@RestController
@RequestMapping("/pet-store") // top-level URI component
class PetStoreController {
  @GetMapping("/cart-items")
  fun getCartItems(
    // Filterable by query parameters
    @RequestParam quantity: Int? = null,
    @RequestParam item: ItemId? = null,
    @RequestParam deliveryAddress: String? = null,
  ): List<CartItem>
 
  @PostMapping("/cart-items")
  fun postItemToCart(
    // What to add to the cart is sent in the request body
    @RequestBody item: ItemId,
    @RequestBody quantity: Int,
  )
 
  @GetMapping("/inventory/{itemId}")
  fun getQuantityOfItem(@PathVariable item: ItemId): Int
}

Leveraging OpenAPI Generator

The OpenAPI generator can be used for generating Controllers, which enables us to write our APIs spec-first, which is preferred, as clients can be generated in a variety of languages, as outlined in a previous article.

The generator will produce code similar to the hand-rolled Controller above, but all from a YAML specification of the API, rather than needing developers to write all of the annotation boilerplate. Here’s an example of the above Controller, written as an OpenAPI spec:

openapi: 3.0.0
info:
  title: Pet Store API
  description: API for managing a pet store's inventory and cart
 
paths:
  /pet-store/cart-items:
    get:
      summary: Get cart items
      parameters:
        - in: query
          name: quantity
          schema.type: integer
        - in: query
          name: item
          schema.type: string
          description: Filter by item ID
        - in: query
          name: deliveryAddress
          schema.type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CartItem'
 
    post:
      summary: Add item to cart
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                item:
                  type: string
                quantity:
                  type: integer
      responses: '200'
 
  /pet-store/inventory/{itemId}:
    get:
      summary: Get quantity of item
      parameters:
        - in: path
          name: itemId
          required: true
          schema.type: string
      responses:
        '200':
          content:
            application/json:
              schema.type: integer
 
components:
  schemas:
    CartItem:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        quantity:
          type: integer

References

  1. https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/
  2. https://www.ibm.com/topics/rest-apis
  3. https://blog.postman.com/rest-api-examples/