Step by step writing an API endpoint using Kotlin and SpringBoot
When implementing a new rest API you are at the beginning in the situation that you might know what the consumer sends as payload and expect as result of an API call but you didn’t exactly know how the way from specification to implementation is done.
If you are following the TDD principles and start by writing a test for every requirement first and then add the necessary implementation, this helps to evolve a good architecture but it can be difficult to keep the focus on what the consumer of the API expects. Because the final architecture may not be the first draft, refactoring should be possible without changing the behavior for the consumer.
For this it can be helpful to start with an acceptance test before even writing any line of productive code. Acceptance test means for me, a test which is calling the rest API endpoint using MockMvc but using all other involved components as in production.
In this article I will show a step by step development of an API endpoint guiding from requirement until final implementation. I will show the final implementation for every step. For having a more detailed overview about the small steps I’ve done, please have a look at the repository of the project (see at the end of the article).
For my implementation I use Kotlin + SpringBoot (JPA for persistence) + MySQL database.
Requirement for API endpoint
There are following requirements specified:
- Url: /api/v1/customer
- Payload: see below
- Validation (Name must not contain special characters, age must be between 18 and 100, mail address must be unique, zip code must consist of 5 digits, country must be in ISO conform format)
- Response: customer id unique (5 digits) with http status 201
"street": "Main Street",
"city": "Los Angeles",
Limitations of the sample project
- Logging will be omitted (can easily be added if wanted)
- Exception handling will be very basic and exemplary
- Tests are reduced to the important cases
- Business logic is simplified, no real world example
The intention of my example is to show a way how to develop rest API endpoints, not to cover all potential challenges a productive application adds. I want to show in which order I develop the components which are necessary to fulfill the use case. The architecture and the order of implementation are the important parts.
Step 1: Writing acceptance test:
For the guiding acceptance test I use @MockMvc in combination with @SpringbootTest and TestContainers framework. Therefore I create a custom annotation which can be used to simplify the test setup (see https://medium.com/@inzuael/write-custom-annotation-for-testing-springboot-application-with-real-database-813173ee06b5 for detailed information about the setup). This offers me the possiblity to run my tests against a real MySQL database inside a docker container.
When running the acceptance test using the productive components offers the advantage that productive code is executed and no mock/fake functionality needs to be used. After a phase of heavily using mocks for testing applications I meanwhile use them very conscious mainly for exceptional test cases (which cannot be reproduced easily) and for reducing runtime of tests using long running external systems (like mailing. file generation).
When running the test in IntelliJ I get the expected result:
There is no endpoint available for the requested url and a http status 404 (Not Found) is returned instead of 201 (Created).
This test is he guidline for writing the implemention. After every implementation step I will get the feedback, if the use-case is fulfilled and I reached my goal or an other step is necessary.
Step 2: Add domain objects
For simplicity I use domain objects also as jpa entities. Adding an additional layer of abstraction between will increase the complexity.
I use Kotlins data class for modeling the domain entities.
The validation according to the specification is done in a later step.
Step 3: Add repositories
In order to test the table configuration and check if the persistence layer is working as expected, I add the repositories next. Using the CrudRepository interface which spring data offers makes this step very easy without writing too much boilerplate code.
Custom queries can easily be written using the derivation mechanism of spring data (see https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-keywords).
For testing the repositories I use the custom @RealDatabaseTest annotation which I created before.
Step 4: Add constraints + validation for domain objects
As the next step I will add the unique constraint for the email address which can easily be done with @Column annotation of jpa.
To ensure that it is not possible to create a domain object which is not valid according to the specification, I add a init block to the domain objects which validates the object during creation time. If creation of object is successful, I can guarantee the result is a valid domain object.
I also add a simple mechanism (enough for the sample project) for creation of unique customer id in required lenght.
For validation of the domain object generation I add unit tests, the check for unique email, which is validated by database, is done by a classical integration test.
Step 5: Add application service
As the next step the application service which orchestrates the workflow between the rest controller and the persistence to database is introduced. There are the following tasks the service has to fulfill:
- Mapping of data transfer objects (DTO) to domain objects
- Validate input
- Transfer data to persistence layer
- Create result object depending on status
The error handling for the use case is completely done in application service. There is no spreading of handling exceptions in all layers. The layers below and above the application service not need to deal with try-catch blocks, they just throw appropriate exception types. There is a generic catch block for Exception in order to be able to also get unexpected exceptions (this is maybe not necessary for the sample application, but if there are calls to external services, this is very helpful).
The application service is returning a result object wrapping the success and the failure case. In order to limit the results to explicit 2 types I use a sealed class with 2 implementations.
Step 6: Add rest controller
The last step necessary for getting my acceptance test running is the addition of a rest controller which handles the corresponding requests.
The rest controller is mapping the api result which is returned from application service to http result depending on typ. If the error type is returned, the http status depends on returned error code. In case of success the customerId is wrapped in a success object.
After adding the rest controller the acceptance test is running successfully:
Following this steps for developing API endpoints leads to a good separation of layers. Each part can be tested separately and exchange of layers is also possible.
see https://github.com/PoisonedYouth/kotlin-acceptance-test for the sample application code.