Recently, I had to create an API Gateway for a customer. Coming from a developer background, API design was an afterthought for me. Once I was done with the user stories, I wrote my database schema so I could go ahead with coding the web application. The API develops organically, from whatever makes sense at that time.
But as I learned on the project, APIs are interfaces between people (as much as they are between systems). Deliberately thinking about your API design and being consistent about these design decisions help your end-users learn your APIs faster and with minimal effort. Also, your end-users rely on your APIs to create applications. Breaking changes to these APIs means downtime for your end user's applications.
Post Summary
In this post, we will discuss how the API-first mindset encourages us to put our end-user first so they can count on a simple, reliable, and consistent API.
In summary, the process looks like this:
-
Part 1 - Create Reliable & Easy-to-use APIs with API First Design (current post)
- Start with the goal
- Create user stories
- Build the domain models
- Sketch your APIs
-
Part 2 - Define API contracts with Swagger / OpenAPI
- Write the OpenAPI definition
- Start Developing
- Handling API Design Changes
Using OpenAPI 3.0 definitions as "contracts" between API developer and his/her end-users, a single source of truth is established on how the API should operate. With this "contract" defining how each endpoint of the API operates, we safeguard against sudden unplanned changes on the API.
API-First Design Process
Let's go through the design process for a simple loyalty application we will be creating for restaurants.
[1] Start with the goal
Our goal for the loyalty application is for partner restaurants to create loyalty cards for customers and for customers to be able to earn points and redeem items with their points.
Setting the goal sets the stage for the next steps of the process. It also helps you scope out what your API really does. If you need to develop features that aren't aligned with this goal, then you should probably consider splitting it off to a separate application.
[2] Create user stories
With this goal in mind, let's elaborate on the capabilities of our loyalty application using user stories:
- As an end-user:
- I can register for a loyalty card online
- I can sign in
- I can earn points with every transaction with a partner restaurant
- I can view my points balance online
- I can view my transaction history online
- I can view specific transactions that I made
- As a restaurant manager:
- I can see online card registrations for my restaurant
- I can print a card for each registration
- I can mark the card as claimed
- I can view transactions consummated with the loyalty card for my restaurant
- I can use a user's points to transact
- As an admin user:
- I can create restaurants
[3] Build the domain models
Building your domain models is the process of translating your user stories into objects. These objects have attributes and behaviors. Think of these classes as similar to classes in the Object-Oriented Programming (OOP) paradigm.
With the objects in place, let's think about their relationships with one another:
- User can have many cards
- Card can have many transactions
- Partners can originate many cards (they can sell as many loyalty cards as they want)
- Partners can have many transactions
To operationalize these relationships, we will introduce the concept of "foreign key". With a foreign key, we add the field {object_name}_id
to the object at the right side of the relationship. For the "User can have many cards" relationship, we add the field user_id
on the Card object.
[4] Sketch your APIs
Once you have the domain models ready, it's time to sketch your APIs. Resist the urge to work at your web application right away. Take the time to think about each API endpoint you are going to expose and the resource layout of your APIs.
Sketching your API forces you to imagine how your API is going to look before you put in the long dev hours. After working on the sketch, show this design to your API stakeholders and get their feedback. At this stage, major changes will be almost costless to do because no code has been implemented yet.
Understanding API endpoints
To call an API, send a request to the server with the following components:
- base URL: loyalty-app.com
- path: /cards
- How we construct our paths determines how our resource layout is going to be. An alternative resource layout for the Card resource is to nest it with the User resource, such as /users/10/cards.
- HTTP method: GET
- query parameters: ?name=Raphael
- request body:
- It is used for POST and other HTTP methods, but not GET.
- headers
For our example above, when you join them together, the request should look like this:
GET loyalty-app.com/cards?name=Raphael
API Best Practices
Here are a few guidelines. Most of these tips are not hard rules. At the end of the day, you will have to decide what is best for your API. But whatever you decide, you should aim to be consistent in your API design.
Standard Methods
- Standard methods allow APIs to have a consistent set of methods across different resources.
- Standard methods are patterned after CRUD (Create, Read, Update, Delete). A list of the standard methods is available below.
- Standard methods should have no side effects. It does what it says, nothing more. This way, we can have a consistent set of expectations across all standard methods. For example, the
POST /loyalty-cards
endpoint creates a loyalty card, it should not create a transaction as well. - Itβs preferable to use PATCH over PUT when updating only certain parts of an object.
- If you're not going to implement all standard methods of a resource, you should still build an endpoint for it but have it return HTTP 405 (method not allowed) or HTTP 403 (forbidden). This way, your API is consistent.
Custom Methods
- Custom methods allow you to create endpoints with side effects. It has the format
/{resource}/{id}:{custom_action}
- For example, the
POST /loyalty-cards/10:claim_card
- For example, the
Use HTTP Status Codes
- Use HTTP status codes to communicate meaning. Not all errors are error 500 and not all successful operations should be HTTP 200.
- HTTP 200:
- HTTP 201 (Created): After creating a resource
- HTTP 204 (No-Content): After deleting a resource
- HTTP 200 (OK): Catch-all for successful operation
- HTTP 400: User error - the user tried something wrong
- HTTP 403 (Forbidden): When you don't have access to the resource you are accessing
- HTTP 404 (Not Found):
- HTTP 400 (Bad Request): Check the full list of HTTP 400 codes, if it's not there use this as a catch-all
- HTTP 500: Server error - the fault is with the server
- HTTP 500: Internal Server Error - pretty much a catch-all for all application errors.
- HTTP 200:
Different approach for long-running operations
- As a best practice, these guidelines apply to API endpoints that consistently return faster than 30secs. If your endpoint takes longer to respond, you will keep your end-users waiting and this will result in a subpar user experience. Instead of using the fast response paradigm we have introduced thus far, you may have to look into the asynchronous processing paradigm.
That's a quick summary of the API guidelines. If you want to learn more, I highly recommend the book API Design Patterns by JJ Geewax.
For a complete listing of standard endpoints, here is a listing using the cards resource:
standard - collection endpoints
- CREATE - POST /cards
- READ ALL - GET /cards
standard - resource endpoints
- READ ONE - GET /cards/10
- UPDATE ONE (override object) - PUT /cards/10
- UPDATE ONE (override specific methods) - PATCH /cards/10
- DELETE ONE - DELETE /cards/10
Let's apply what we have learned
Our task now becomes determining our API resource layout and translating the methods of each class into an API endpoint.
For the Cards class, we chose to nest it to the users resource. Hierarchial relationships like this are created when the nested resource is owned by the parent resource. This means if we delete our user #10, we also delete all the cards associated with it. If user #10 abused our fair use policy, we disable all his cards, and so on.
What's next?
Now, we have successfully created our API design sketch. At this stage, I suggest you show this sketch to your API stakeholders. Get their feedback and iterate based on that.
In the next post, we will be creating an Open API 3.0 definition to define the details of each API endpoint. With this definition, we can have a mock
Special Thanks
Special thanks to Allen for making my posts more coherent. This blog post is also made possible by the authors below who have made learning APIs a joy.
- API Design Patterns by JJ Geewax
- Designing APIs with Swagger and OpenAPI by Joshua S. Ponelat and Lukas L. Rosenstock
- Design and Build Great Web APIs by Mike Amundsen
Top comments (10)
Iβve never seen custom resource methods like that (with a colon after resource Iβd), but I quite like the idea. Do you have any examples of other APIs that use that technique? Iβm wondering how widespread it is. Thanks
Hi Pete! I got the idea from JJ Geewax's API Design Patterns Book. He is a software engineer at Google, working on GCP. Google Cloud Platform is one of the companies using this now: cloud.google.com/apis/design/custo...
I like the idea too. By using colons instead of the famous slash character, we get to have a clear separation between the resource layout (/users/10/transactions/11) and the custom method (:mark_as_paid, with the whole URL being /users/10/transactions/11:mark_as_paid).
It takes some getting used to, but I think it results in better, more consistent API design.
I especially appreciate the HTTP status codes part.
Someone where I work, actually told me that other developers would "laugh at us" for using any status code besides 200 OK. He thought that returning result codes (RC) and result messages (msg) is enough info for other developers, and when it comes to status code, he thinks if the request passes through, then it should be 200 OK.
Obviously this is PAINFULLY wrong, and it was more painful because that person was treated like a GOD in the company, and his title had the words "Expert Web Engineer" in it...
Nevertheless, I managed to convince him with proofs from all around the world.
Hi Rob! I totally agree with you! When I did API testing for a client, it really annoyed me to see all of their APIs returned HTTP 200, even if there was an error! They just use the presence of the key "error_message" on the response to communicate that there was an error.
I think this is a complete waste of the HTTP code functionality and how a lot of HTTP clients are already preconfigured to handle 200s, 300s, 400s, and 500s differently.
Thanks, Raphael! This article somewhat gives a clearer picture of what I'm building now. I knew I was missing a step that's why I'm all over the codebaseπ€£
Hi Vicente! Glad that the article was helpful to you. Let me know if there's anything else I can help with!
And happy to see more Filipino kababayans here in Dev.to! π΅π
Sure man! Looking forward to more articles from you! π΅π π΅π π΅π π΅π
Thanks Vicente!
I love the fact that you talk about the person. It might be a computer doing the talking but itβs a person writing the code.
Hi Rahoul! Thank you for the comment. I would like to think of API as an interface between people (tech teams) as much as they are between systems. Looking at the APIs we create as the "gateway" of other teams into our systems or companies can really go a long way in appreciating why good and consistent API design is a must.