Debts Manager Tutorial Part 2: Contract Design
Hi guys! Welcome back to this tutorial!
In this second chapter of Debts Manager Tutorial I would like to show you how I have designed the REST API of Debts Manager. I’m going to follow the API First approach, documenting all aspects of the API Design with OpenAPI 3.
This post doesn’t aim to provide you a full guide of how to design REST APIs: if you want more resources to learn it, look at the end of this post
Analysis
The REST APIs, in contrast with RPC, are driven by the data the services wants to expose. In the previous chapter I gave you an idea of the entities we must expose. Now I tabulate these and the relative operations on it.
Entity | Create | Retrieve | Update | Delete |
---|---|---|---|---|
User | ✔ | ✔ | ❌ | ❌ |
User relationship | ✔ | ✔ | ❌ | ❌ |
Transaction | ✔ | ✔ | ✔ | ✔ |
Status | ❌ | ✔ | ❌ | ❌ |
This table is a pretty good starting point, but I must refine the analysis enforcing our methods with policies and logics.
These policies are primarly based on who is making the request. I’m going to define a login phase together with JWT to provide authorization and authentication. Each endpoint, except login
and register
, is secured with a JWT auth. My objective is expose, for each user, only a subset of data relative to the user itself.
Models
Before defining the endpoints I must formally describe the data models representing the service entities. OpenAPI has its own Json Schema dialect to define models: OpenAPI Schema. This is an extended subset of Json Schema Draft 5. Meanwhile I’m writing, there is a proposal to allow usage of every version of Json Schema, including the newer versions, with an extension https://github.com/OAI/OpenAPI-Specification/issues/1532.
I place these schemas in main OpenAPI file under components
and schemas
keywords. I can refeer to it using Json schema references ($ref
keyword).
The simplest model here is the user. I want to expose only the username, so I represent it with a simple string
. This is the definition using OpenAPI Schema:
1 | Username: |
The status is represented by a map with users as keys and total debts\credits as values. In OpenAPI Schema:
1 | Status: |
In JSON maps are usually represented or as json array of tuples key-value or as a json object. The json object is the natural way to represent it, but it has an important restriction: keys are strings. In my case I need to represent a map string → number, so json object representation fits good. The map values schema are defined using additionalProperties
and, only with Json Schema Draft 7 or newer, keys schema are defined using propertyNames
.
The main transaction model is described below:
1 | Transaction: |
$ref
keyword points to the Username
schema I defined before.
This model doesn’t fit good for my usage, because for each Transaction
endpoint I want to apply some policies. A very common example is the id
field: when user inserts a new transaction I want to designate the database to fill the id
value. When the user creates a new transaction it shouldn’t add the id
field: that means that I can’t use the Transaction
model to describe the “create transaction” request body. Let’s look at all restrictions I want to apply on various transaction endpoints:
id
andat
are filled by the backend when user adds a new transaction and they are immutable from the API perspective- When user updates a transaction he can’t update the
from
(sender) andto
(receiver) fields - When user adds a new transaction he doesn’t need to fill the
from
field because the backend fills it with the logged user
To apply these restrictions I create a new model for each endpoint. I’m going to refactor Transaction
into 3 different models: UpdateTransaction
, NewTransaction
and Transaction
.
These new models lead to a new problem: duplication of model fields definitions. Json schema solves the duplication with schema composition keywords: allOf
, anyOf
and oneOf
. In particular I will use allOf
to achieve inheritance of schemas.
This is the final result:
1 | UpdateTransaction: |
The schemas inheritance tree is UpdateTransaction
←NewTransaction
←Transaction
Endpoints
OpenAPI document structures the endpoint definitions as follow:
1 | paths: |
OpenAPI path strings allow path parameters using {paramName}
and doesn’t require an explicit definition of query parameters.
In OpenAPI terminology an operation is an API endpoint identified by a path and an HTTP method. Every operation could be uniquely identified with an operationId
. The OpenAPI Specification (OAS) documents this field as optional, but I strongly suggest to specify it if you don’t want to see your tooling explode. Most code generation tooling asserts that operationId
is present. If it’s not present they try to infeer it from path and http method producing unexpected results.
For each operation we are going to define:
operationId
parameters
(if any): List ofheader
,path
,query
andcookie
parametersrequestBody
(if any): Content type and content schema of request bodiesresponses
: Status code with response content type and schemas
I also fill the security
field for each operation to require a JWT token to execute it.
Transactions and Status
Let’s start with transaction CRUDs:
Operation | operationId |
CRUD | Path | HTTP Method |
---|---|---|---|---|
Create a new transaction | createTransaction |
Create | /transactions |
POST |
Get a single transaction | getTransaction |
Retrieve | /transactions/{transactionId} |
GET |
Get user related transactions | getTransactions |
Retrieve multiple | /transactions |
GET |
Update a transaction | updateTransaction |
Update | /transactions/{transactionId} |
PUT |
Delete a transaction | deleteTransaction |
Delete | /transactions/{transactionId} |
DELETE |
In OpenAPI:
1 | /transactions: |
Note that for all operations under /transactions/{transactionId}
path I haven’t redefined every time the parameter transactionId
: I have defined once at path level.
Status has only the retrieve operation, but I want to let user customize the output based on transactions insertion datetime: clients can use query parameter till
to ask the status till the date time provided, excluding newer transactions. You can use it to throw back in your house mate face that he didn’t pay the bills for a quite long time.
1 | /status: |
User and User relationships
The service supports creation and retrieval of users and user relationships. For simplicity I avoided to include U and D operations for user and user relationships.
I want to expose an endpoint to retrieve all registered users and an endpoint to retrieve only users that have a relationship with logged user:
1 | /users: |
In getUsers
we defined a very basic search functionality with the optional query parameter filter
In getConnectedUser
I prefeered to define the request schema directly inside the request body definition because It’s a schema strictly related to this operation and It isn’t parent of any other schema.
This is the endpoint to create a user connection (user relationship):
1 | /users/connected/{userToConnect}: |
Login, registration and JWT
When an user wants to start using this API he must authenticate with his credentials following this process:
- User calls the
/login
endpoint passing his credentials in the request body - The backend checks if credentials are correct
- The backend writes the response with a JWT token containing the username inside the payload
- User stores the received JWT token
For each request the server must authorize the user. The user must include inside each request the header Authorization: Bearer <jwt token>
. When the backend receives the request it checks the signature validity and the token expiration time. If the token is valid It parses the payload, where It can read the username of the logged user.
This is the login
operation definition:
1 | /login: |
The register
operation creates a new user and logins it:
1 | /register: |
I don’t cover in this tutorial the logout process, but I want to give you a tip: create a whitelist or blacklist of tokens.
As you already saw, each secured operation has the security
field:
1 | security: |
The security
field is called security requirement and it tells the user that he needs loggedUserToken
security schema to access to this endpoint. Security schemas must be defined under #/components/securitySchemes
:
1 | securitySchemes: |
Some resources to learn Web API Design and OpenAPI
I give you a couple of useful links:
- Rest API Tutorial: Very simple and coincise tutorial for newbies of REST APIs world
- API Stylebook: Collection of API styleguides from different IT companies
- OpenAPI Specification repository: Contains the spec and examples
- OpenAPI Directory: Collection of OpenAPIs of different public APIs
Conclusion
You can find the complete OpenAPI definition here: /src/main/resources/debts_manager_api.yaml
After you learnt how to design a REST API, approacching to OpenAPI is very simple. The operation definition is very intuitive because of 1:1 mapping with HTTP (methods, parameters, status codes, content types and so on). The tricky and magic part, for me, is definining and organizing the JSON Schemas. When you define simple models, you tend to put everything inside the same file. But when you raise the complexity using composed schemas, you get flooded by smaller and unclear schemas. My suggestion for you is to document the schemas with title
and description
keywords and organize these in multiple files.
In next chapter I’m going to bootstrap the project and start writing first Vert.x code, stay tuned!