RESTful APIs
Last updated
Last updated
RESTful API controllers are a liaison between the core business logic layer and the outside world. They sit on the other side of the core business realm of any application. In a way, API Controllers are just like Brokers. They ensure a successful integration between our core logic and the rest of the world.
Controllers sit at the edge of any system. Regardless whether this system is a monolithic platform or simple microservice. API controllers today even apply to smaller lambdas or cloud functions. They play the role of a trigger to access these resources in any system through REST.
The consumer side of controllers can vary. In production systems these consumers can be other services requiring data from a particular API endpoint. They can be libraries built as wrappers around the controller APIs to provide a local resource with external data. But consumers can also be just engineers testing endpoints, validating their behaviors through swagger documents.
There are several rules and principles that govern the implementation of RESTful API endpoints. Let's discuss those here.
Controllers speak a different language when it comes to implementing their methods as compared to services and brokers. For instance, if a broker that interfaces with a storage uses a language such as InsertStudentAsync
, and its corresponding service implementation uses something like AddStudentAsync
the controller equivalent will be using RESTful language such as PostStudentAsync
.
There are only a handful of terminologies a controller would use to express a certain operation. Let's draw the map here for clarity:
Controllers | Services | Brokers |
---|---|---|
The language controllers speak is called Http Verbs. Their range are wider than the aforementioned CRUD operations. For instance, there is PATCH which allows API consumers to update only portions of a particular document. PATCH is rarely used today from my experience in productionized applications. But I may specialize a special section for them at some point in time in future versions of The Standard.
But as we mentioned before, controller can interface with more than just a foundation service. They can interface with higher-order business logic function. For instance, a processing service may offer an Upsert
routine. In which case a typical Http Verb wouldn't be able to satisfy a combinational routine such as an Upsert
. In which case we resolve to the intial state of Post
assuming the resource doesn't exist.
It may become useful to notify our consumers if we decided to modify instead of add which operation we decided to go with. But that's a case by case implementation and more often than ever, consumers don't really care to learn that piece of information. The same idea applies to other languages non-foundation services may use. Such as Process
or Calculate
or any other business-specific language higher or hyper advanced order services may choose.
Sometimes, especially with basic CRUD operations, you will need the same Http Verb to describe two different routines. For instnace, integrating with both RetrieveById
and RetrieveAll
both resolve to a Get
operation on the RESTful realm. In which case each function will have a different name, while maintainig the same verb as follows:
As you can see above, the differentiator here is both the function name GetAllStudents
versus GetStudentByIdAsync
but also the Route
at the same time. We will discuss routes shortly, but the main aspect here is the ability to implement multiple routines with different names even if they resolve to the same Http Verb.
RESTful API controllers are accessible through routes. a route is simply a url that is used combined with an Http Verb so the system knows which routine it needs to call to match that route. For instance, if I need to retrieve a student with Id 123
then my api route would be as follows: api/students/123
. And if I want to retrieve all the students in some system, I could just call api/students
with GET
verb.
3.1.1.2.0.2.0 Controller Routes
The controller class in a simple ASP.NET application can be simply setup at the top of the controller class declaration with a decoration as follows:
The route there is a template that defines the endpoint to start with api
and trailed by omitting the term "Controller" from the class name. So StudentsController
would endup being api/students
. It's important that all controllers should have a plural version of the contract they are serving. Unlike services where we say StudentService
controllers would be the plural version with StudentsController
.
3.1.1.2.0.2.1 Routine Routes
The same idea applies to methods within the controller class. As we say in the code snippet above, we decorated GetStudentByIdAsync
have had an HttpGet
decoration with a particular route identified to append to the existing controller overall route. For instance if the controller route is api/students
, a routine with HttpGet("{studentId})
would result in a route that looks like this: api/students/{studentId}
.
The studentId
then would be mapped in as an input parameter variable that must match the variable defined in the route as follows:
But sometimes these routes are not just url parameters. Sometimes they contain a request within them. For instance, let's say we want to post a library card against a particular student record. Our endpoint would look something like this: api/students/{studentId}/librarycards
with a POST
verb. In this case we have to distinguish between these two input parameters with proper naming as follows:
3.1.1.2.0.2.2 Plural Singular Plural
When defining routes in a RESTful API, it is important to follow the global naming conventions for these routes. The general rule is to access a collection of resources, then target a particular entity, then again acess a collection of resources within that entity and so on and so forth. For instance, in the library card example above api/students/{studentId}/librarycards/{librarycardId}
you can see we started by accessing all students, then targetted a student with a particular id, then we wanted to access all library cards attached to that student then target a very particular card by referencing its id.
That convention works perfectly in one-to-many relationships. But what about one-to-one relationships? Let's assume a student may have one and only one library card at all times. In which case our route would still look something like this: api/students/{studentId}/librarycards
with a POST
verb, and an error would occur as CONFLICT
if a card is already in place regardless whether the Ids match or not.
3.1.1.2.0.2.2 Query Parameters & OData
But the route I recommend is the flat-model route. Where every resource lives on it's own with it's own unique routes. In our case here pulling a library card for a particular student would be as follows: api/librarycards?studentId={studentId}
or simply use a slightly advanced global technology such as OData where the query would just be api/librarycards?$filter=studentId eq '123'
.
Here's an example of implementing basic query parameters:
On the OData side, an implementation would be as follows:
The same idea applies to POST
for a model. instead of posting towards: api/students/{studentId}/librarycards
- we can leverage the contract itself to post against api/librarycards
with a model that contains the student id within. This flat-route idea can simplify the implementation and aligns perfectly with the overall theme of The Standard. Keeping things simple.
Responses from an API controller must be mapped towards codes and responses. For instance, if we are trying to add a new student to a schooling system. We are going to POST
student and in retrun we receive the same body we submitted with a status code 201
which means the resoruce has been Created
.
There are three main categories where responses can fall into. The first is the success category. Where both the user and the server have done their part and the request has succeeded. The second category is the User Error Codes, where the user request has an issue of any type. In which case a 4xx
code will be returned with detailed error message to help users fix their requests for perform a successful operation. The third case is the System Error Codes, where the system has run into an issue of any type internal or external and it needs to communicate a 5xx
code to indicate to the user that something internally have gone wrong with the system and they need to contact support.
Let's talk about those codes and their scenarios in details here.
Success codes either indicates a resource has been created, updated, deleted or retreived. And some cases it indicates that a request has been submitted successfully in an eventual-consistency manner that may or may not succeed in the future. Here's the details for each:
Here's some examples for each:
In a retrieve non-post scenario, it's more befitting to return an Ok
status code as follows:
But in a scenario where we have to create a resource, a Created
is more befitting for this case as follows:
In eventual consistency cases, where a resource posted isn't really persisted yet, we enqueue the request and return an Accepted
status to indicate a process will start:
The Standard rule for eventual consistency scenarios is to ensure the submitter has a token of some type so requestors can inquire about the status of their request with a different API call. We will discuss these patterns in a different book called The Standard Architecture.
This is the second category of API responses. Where a user request has an issue in it and the system is required to help the user understand why their request was not successful. For instance, assume a client is submitting a new student to a schooling system. If the student Id is invalid a 400
or Bad Request
code should be returned with a problem detail that explains what exactly is the reason for the failure of the request.
Controllers are responsible for mapping the core layer categorical exceptions into proper status codes. Here's an example:
So as shown in this code snippet, we caught a categorical validation exception and mapped it into a 400
error code which is BadRequest
. The access to inner exception here is for the purpose of extracting a problem detail out of the Data
property on the inner exception which contains all the dictionary values of the error report.
But sometimes controllers have to dig deeper. Catching a particular local exception not just the categorical. For instance, say we want to handle NotFoundStudentException
with an error code 404
or NotFound
. Here's how we would accomplish that:
In the code snippet above, we had to examine the inner exception type to validate the localized exception from within. This is the advantage of the unwrapping and wrapping process we discussed in section 2.3.3.0.2 of The Standard. Controller may examine multiple types within the same block as well as follows:
With that in mind, let's detail the most common mappings from exceptions to codes:
There are more 4xx
status codes out there. But As of this very moment they can either be automatically generated by the web framework like in ASP.NET or there are no useful scenarios for them yet. For instance, a 401
or Unauthorized
error can be automatically generated if the controller endpoint is decorated with authorization requirement.
System error codes are the third and last possible type of codes that may occur or be returned from an API endpoint. Their main responsibility is to indicate in general that the consumer of the API endpoint is at no fault. Something bad happened in the system, and the engineering team is required to get involved to resolve the issue. That's why we log our exceptions with a severity level at the core business logic layer so we know how urgent the matter may be.
The most common http code that can be communicated on a server-side issue is the 500
or InternalServerError
code. Let's take a look at a code snippet that deals with this situation:
In the above snippet we completely ignored the inner exception and mainly focused on the categorical exception for security reasons. Mainly to not allow internal server information to be exposed in an API response other than something as simple as Dependency error occurred, contact support.
Since the consumer of the API is required to perform no action whatsoever other than creating a ticket for the support team.
Ideally, these issues should be caught out of Acceptance Tests which we will discuss shortly in this chapter. But there are times where there's a server blip that may cause a memory leakage of some sort or any other internal infrastrucrual issues that won't be caught by end-to-end testing in any way.
In terms of types of exceptions that may be handled, it's a little smaller when it comes server error here's the details:
There's also an interesting case where two teams agree on a certain swagger document, and the back-end API development team decides to build corresponding API endpoints with methods that are not yet implemented to communicate to the other team that the work hasn't started yet. In which case using error code 501
is sufficient which is just a code for NotImplemented
.
It is also important to mention that the native 500
error code can be communicated in ASP.NET applications through Problem
method. We are relying on a library RESTFulSense
to provide more codes than the native implementation can offer, but more importantly provide a problem detail serialization option and deserialization option on the client side.
Other than the ones mentioned in previous sections, and for documentation purposes, here's the all of the 4xx
and 5xx
codes an API could communicate according to the latest standardized API guidelines:
We will explore incorporating some of these codes in future revisions of The Standard as needed.
Exposer components can have one and only one dependency. This dependency must be a Service component. it cannot be a Broker or any other native dependency that Brokers may use to pull configurations or any other type of dependencies.
When implementing a controller, the constructor can be implemented as follows:
This charactristic comes out of the box with the single dependency rule. If Services can only serve and receive one contract then the same rule will apply to controllers. They can return a contract, a list of objects with the same contract or portion of the contract when passing in Ids or queries.
Controllers should be located under Controllers
folder and belong within a Controllers
namespace. Controller do not need to have their own folders or namespaces as they perform a simple exposure task and that's all.
Here's an example of a controller namespace:
Every system should implement an API endpoint that we call HomeController
. The controller only responsibility is to return a simple message to indicate that the API is still alive. Here's an example:
Home controllers are not required to have any security on them. They open a gate for heartbeat tests to ensure the system as an entity is running without checking any external dependencies. This practice is very important to help engineers know when the system is down and quickly act on it.
Controllers can be potentially unit tested to verify the mapping of exceptions to error codes are in place. But that's not a pattern I have been following myself so far. However, what is more important is Acceptance tests. Which verify all the components of the system are fully and successfully integrated with one another.
Here's an example of an acceptance test:
Acceptance tests are required to cover every available endpoint on a controller. They are also responsible for cleaning up any test data after the test is completed. It is also important to mention that resources that are not owned by the microservice like database, must be emulated with applications such as WireMock
and many others.
Acceptance tests are also implemented after the fact unlike unit tests. An endpoint has to be fully integrated and functional before a test is written to ensure the success of implementation is in place.
Code | Method | Details |
---|---|---|
Code | Method | Exception |
---|---|---|
Code | Method | Exception |
---|---|---|
Status | Code |
---|---|
200
Ok
Used for successful GET, PUT and DELETE operations.
201
Created
Used for successful POST operations
202
Accepted
Used for request that was delegated but may or may not succeed
400
BadRequest
ValidationException or DependencyValidationException
404
NotFound
NotFoundException
409
Conflict
AlreadyExistException
423
Locked
LockedException
424
FailedDependency
InvalidReferenceException
500
InternalServerError
DependencyException or ServiceException
507
NotFound
InsufficientStorageException (Internal Only)
BadRequest
400
Unauthorized
401
PaymentRequired
402
Forbidden
403
NotFound
404
NotFound
404
MethodNotAllowed
405
NotAcceptable
406
ProxyAuthenticationRequired
407
RequestTimeout
408
Conflict
409
Gone
410
LengthRequired
411
PreconditionFailed
412
RequestEntityTooLarge
413
RequestUriTooLong
414
UnsupportedMediaType
415
RequestedRangeNotSatisfiable
416
ExpectationFailed
417
MisdirectedRequest
421
UnprocessableEntity
422
Locked
423
FailedDependency
424
UpgradeRequired
426
PreconditionRequired
428
TooManyRequests
429
RequestHeaderFieldsTooLarge
431
UnavailableForLegalReasons
451
InternalServerError
500
NotImplemented
501
BadGateway
502
ServiceUnavailable
503
GatewayTimeout
504
HttpVersionNotSupported
505
VariantAlsoNegotiates
506
InsufficientStorage
507
LoopDetected
508
NotExtended
510
NetworkAuthenticationRequired
511
Post
Add
Insert
Get
Retrieve
Select
Put
Modify
Update
Delete
Remove
Delete