Guidelines for structuring REST APIs
I have employed the following practices to structure my REST APIs making them:
- Intuitive (For Consumers)
- Isolation of concern (For Developers)
- Inherent Classification (For Consumers and Developers)
- Extensibility (For Developers)
Any RESTFUL application can be classified into following constructs:
- Entities
- Operations (Action to be performed on Entities)
Consider you are designing an Object Storage API similar to AWS S3 that supports operations like:
- Ingest
- Archive
Supported Entities are:
- RDBMS (PostgreSQL, MySQL)
- NoSQL (MongoDB, Redis)
- TimeSeries (InfluxDB)
- Message Queues (RabbitMQ)
Assume, each of these entities have different teams building custom integrations for them.
Now you could design your REST APIs as:
- Operation->Entity (Operation encompass entities)
-
ingest
POST: /ingest/{entity}
Body: {
“type” : “PostgreSQL”
“connection-details” : “”,
“contents” : “”
}
For example:
/ingest/rdbms /ingest/nosql ... etc
-
archive
POST: /archive/{entity}
Body: {
“type” : “PostgreSQL”
“connection-details” : “”,
“contents” : “”
}
-
The Java Controller Class for this will have following structure
Disadvantages of this approach:
- As the entry point in the above code is IngestController.ingestService(), each of the teams will need to update the same method leading to merge conflicts
- The request body params are need to be general to support each of the entities - like nosql, rdbms, etc. Not enforcing strict parameter scope can lead to security threats and erroneous requests.
- Can lead to code smells due to the switch statement growing in size as more entities are added.
- No strict enforcement, consumer can pass an unsupported entity type in the path param.
- As the number of entities grow, the endpoints also grow, leading to cumbersome documentation. So for the consumer looking consume these endpoints will be flooded with a long list of endpoints.
Better approach
- Entity -> Operation (Entity encompass operation)
-
rdbms
POST: /rdbms/ingest Body: { “type” : “PostgreSQL” “connection-details” : “”, “contents” : “” } POST: /rdbms/archive Body: { “type” : “PostgreSQL” “connection-details” : “”, “contents” : “” }
-
nosql
POST: /nosql/ingest Body: { “type” : “MongoDB” “connection-details” : “”, “contents” : “” }
Each Entity will have its own Rest Controller class as follows:
-
Advantages:
- Separate Rest Controller classes so developers can commit and push without merge conflicts.
- Fixed endpoints(no path params) so less chances of invalid requests
- Better grouping as all related operations are encapsulated in same class.
- Better documentation. For example if more operations are added to the object storage APIs like:
- search
- configure
- index
Then they will grouped as
/rdmbs/ingest /archive /search /configure /index /nosql/ingest /archive ...
- Each Entity is free to define it’s own parameters. Hence you can enforce strict parameter scope. This is better for security and validations.
- Lesser code smells
Side-note: When you need to group your APIs under an umbrella (be it entity/functional unit/workflow etc) make sure your umbrella/parent grows at a slower rate than your inner children. This gives a better structuring and visibility as your application grows.
Hopefully more to follow in this series on ‘Designing REST APIs’.
Comments