Original post from
Expat Dev by Anirban
🏷️
#kotlin
#spring
#springboot
#rest
#microservices
- Overview In this article, we’re going to build a REST API using Spring Boot 2.x and Kotlin. Meanwhile, dedicated support for Kotlin was introduced since Spring Framework 5.0. We can read about the supported features in the official Spring Framework documentation.
In particular, the application will expose data via a REST API and persist data in an embedded H2 database.
- Use Case We’ll build Restful APIs for an employee information application. A user can create, retrieve, update and delete an employee using this application. An employee has an id, username, first name, last name, email id, and date of birth. Moreover, the username of an employee must be unique.
We’ll use an embedded H2 database as our data source along with JPA and Hibernate to access the data from the database.
- Setup We can use the Spring Initializer tool to create a Spring Boot application. Besides, we can read about the other ways that we can create a Spring Boot project in the blog post.
Subsequently, we will add Spring Web, Spring Data JPA, H2 Database, Spring Boot Devtools as the dependencies. So now we have all the basic dependencies in the build.gradle.kts that will allow us to work with Spring Boot and Kotlin:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
view rawbuild.gradle.kts hosted with ❤ by GitHub
The Gradle build file also contains some essential plugins:
The kotlin-spring compiler plugin is a wrapper on top of the all-open plugin. The all-open compiler plugin ensures that the Kotlin classes are annotated and their members are open. This makes it convenient to use libraries such as Spring AOP that require classes to be open unlike Kotlin classes and their members which are final by default.
The kotlin-jpa compiler plugin is a wrapper on top of the no-arg plugin. The no-arg compiler plugin generates a no-argument constructor for classes annotated with Entity, MappedSuperclass, and Embeddable. This ensures that JPA (Java Persistence API) can instantiate the class since it expects a no-argument constructor defined for its entities.
kotlin("plugin.spring") version kotlinVersion
kotlin("plugin.jpa") version kotlinVersion
view rawbuild.gradle.kts hosted with ❤ by GitHub
3.1. Configure Database
We have to configure the H2 database properties so that Spring Boot can create a data source. To define such properties, we can use an external configuration. Furthermore, we can define such external configuration using properties files, YAML files, environment variables, and command-line arguments.
In general, Spring Boot provides the application.properties file to define such database properties by default. The properties file uses a key-value format.
Alternatively, we can use YAML-based configuration files which use hierarchical data. The YAML files significantly help to avoid repeated prefixes and make is more readable compared to the properties file:
spring:
datasource:
driver-class-name: "org.h2.Driver"
url: "jdbc:h2:mem:dev_db"
username: "sa"
password: "sa"
jpa:
hibernate:
ddl-auto: update
show-sql: true
generate-ddl: false
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
view rawapplication.yml hosted with ❤ by GitHub
We can also use the application.yml file to define hibernate properties:
We set the ddl-auto property to update. It will update the database schema based on the modifications in the domain model in our application.
This default value is create-drop if we use an embedded database without any schema manager.
In case we’re using a schema manager such as Liquibase or Flyway, we should consider using validate. It tries to validate the database schema according to the entities that we have created in the application and throws an error if the schema doesn’t match the entity specifications.
The show-sql property enables logging of SQL statement.
The generate-ddl property ensures whether to initialize the schema on startup.
The dialect property ensures that Hibernate generates better SQL for the chosen database.
- Domain Model - Kotlin Data class The domain model is the core part of the application. We can create a domain model Employee using a data class.
In addition, a data class in Kotlin automatically generates equals/hashCode pair, toString, and copy functions. Besides, We don’t need to define getter and setter methods like Java:
@Entity
@Table(name = "employee")
data class Employee (
@id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?,
@Column(name = "user_name", unique = true, nullable = false)
val userName: String,
@Column(name = "first_name", nullable = false)
val firstName: String,
@Column(name = "middle_name", nullable = true)
val middleName: String?,
@Column(name = "last_name", nullable = false)
val lastName: String,
@Column(name = "email_address", nullable = false)
val emailId: String,
@Column(name = "day_of_birth", nullable = false)
val dayOfBirth: LocalDate
)
view rawEmployee.kt hosted with ❤ by GitHub
The @Entity annotation specifies that the class is an entity and mapped to a database table.
The @Table annotation specifies the name of the database table that is used for mapping.
If we use the @Table annotation without a table name, then Spring generates the table name from the class name.
The @id annotation specifies the property which is the primary key in the entity class.
The @GeneratedValue annotation specifies the generation strategies for the values of the primary keys.
The @Column annotation specifies the mapped column for a persistent property. If we do not use the @Column annotation, then Spring generates the column name from the property name.
- Repository We’re going to create a repository interface to interact with the database. Moreover, the EmployeeRepository interface will extend from the JpaRepository interface. This ensures that all the CRUD methods on the Employee entity are available:
@Repository
interface EmployeeRepository : JpaRepository
view rawEmployeeRepository.kt hosted with ❤ by GitHub
The @Repository annotation specifies that the class is a repository and represents the data access layer in our application.
- Service Now, we’re going to create a service class that will provide the business logic. Besides, the service layer can interact with the data access layer using JPA.
We inject an instance of the EmployeeRepository via constructor in EmployeeService:
@Service
class EmployeeService(private val employeeRepository: EmployeeRepository) {
fun getAllEmployees(): List<Employee> = employeeRepository.findAll()
fun getEmployeesById(employeeId: Long): Employee = employeeRepository.findById(employeeId)
.orElseThrow { EmployeeNotFoundException(HttpStatus.NOT_FOUND, "No matching employee was found") }
fun createEmployee(employee: Employee): Employee = employeeRepository.save(employee)
fun updateEmployeeById(employeeId: Long, employee: Employee): Employee {
return if (employeeRepository.existsById(employeeId)) {
employeeRepository.save(
Employee(
id = employee.id,
userName = employee.userName,
firstName = employee.firstName,
middleName = employee.middleName,
lastName = employee.lastName,
emailId = employee.emailId,
dayOfBirth = employee.dayOfBirth
)
)
} else throw EmployeeNotFoundException(HttpStatus.NOT_FOUND, "No matching employee was found")
}
fun deleteEmployeesById(employeeId: Long) {
return if (employeeRepository.existsById(employeeId)) {
employeeRepository.deleteById(employeeId)
} else throw EmployeeNotFoundException(HttpStatus.NOT_FOUND, "No matching employee was found")
}
}
view rawEmployeeService.kt hosted with ❤ by GitHub
The @Service annotation specifies that the class is a service.
The getAllEmployees() function can fetch a list of all employees.
The getEmployeesById() function can fetch one employee based on the id. We provide the employee id as a parameter to the function.
The createEmployee() function can create a new employee. We provide the required properties of the new employee as a parameter to the function.
The updateEmployeeById() function can update the employee details based on the id. We provide both the employee id and the properties as parameters to the function.
The deleteEmployeesById() function can delete an employee based on the id. We provide the employee id as a parameter to the function.
- Controller We implement the APIs for all the CRUD operations in the controller layer which invokes the underlying service.
We define a EmployeeController as our controller class. In particular, it’s a REST controller that provides endpoints for creating, manipulating, and deleting employees.
The EmployeeService class is wired into the controller to return the values. Also, we reuse the entities as data transfer object for simplicity:
@RestController
class EmployeeController(private val employeeService: EmployeeService) {
@GetMapping("/employees")
fun getAllEmployees(): List<Employee> = employeeService.getAllEmployees()
@GetMapping("/employees/{id}")
fun getEmployeesById(@PathVariable("id") employeeId: Long): Employee =
employeeService.getEmployeesById(employeeId)
@PostMapping("/employees")
fun createEmployee(@RequestBody payload: Employee): Employee = employeeService.createEmployee(payload)
@PutMapping("/employees/{id}")
fun updateEmployeeById(@PathVariable("id") employeeId: Long, @RequestBody payload: Employee): Employee =
employeeService.updateEmployeeById(employeeId, payload)
@DeleteMapping("/employees/{id}")
fun deleteEmployeesById(@PathVariable("id") employeeId: Long): Unit =
employeeService.deleteEmployeesById(employeeId)
}
view rawEmployeeController.kt hosted with ❤ by GitHub
The @RestController annotation specifies that the class is a controller and capable of handling requests. It combines both the @Controller and @ResponseBody annotations.
Spring provides several annotations to handle the different incoming HTTP requests like GET, POST, PUT and DELETE. These annotations are @GetMapping,@PostMapping,@PutMapping, and @DeleteMapping.
- Running the application We can run the application using the following command in the terminal:
gradle bootRun
The Spring Boot application will start running at http://localhost:8080.
- Exploring the APIs 9.1. Create an Employee We send the following request to create an employee:
curl --location --request POST 'http://localhost:8080/employees' --header 'Content-Type: application/json' \
--data-raw '{
"userName":"john.doe",
"firstName":"John",
"lastName":"Doe",
"emailId":"john.doe@gmail.com",
"dayOfBirth":"1997-12-03"
}'
view rawpost-request.json hosted with ❤ by GitHub
We receive the following response in JSON format:
{
"id": 1,
"userName": "john.doe",
"firstName": "John",
"middleName": null,
"lastName": "Doe",
"emailId": "john.doe@gmail.com",
"dayOfBirth": "1997-12-03"
}
view rawpost-response.json hosted with ❤ by GitHub
9.2. Get All Employees
We send the following request to fetch all employees:
curl --location --request GET 'http://localhost:8080/employees' --header 'Content-Type: application/json'
view rawget-all-request.json hosted with ❤ by GitHub
We receive the following response in JSON format:
[
{
"id": 1,
"userName": "john.doe",
"firstName": "John",
"middleName": null,
"lastName": "Doe",
"emailId": "john.doe@gmail.com",
"dayOfBirth": "1997-12-03"
},
{
"id": 2,
"userName": "amelia.harrison",
"firstName": "Amelia",
"middleName": "Marie",
"lastName": "Harrison",
"emailId": "amelia.harrison@gmail.com",
"dayOfBirth": "1995-06-03"
}
]
view rawget-all-response.json hosted with ❤ by GitHub
9.3. Get an Employee
We send the following request to fetch an employee based on the employee id:
curl --location --request GET 'http://localhost:8080/employees/1' --header 'Content-Type: application/json'
view rawget-request.json hosted with ❤ by GitHub
We receive the following response in JSON format:
{
"id": 1,
"userName": "john.doe",
"firstName": "John",
"middleName": null,
"lastName": "Doe",
"emailId": "john.doe@gmail.com",
"dayOfBirth": "1997-12-03"
}
view rawget-response.json hosted with ❤ by GitHub
9.4. Update an Employee
We send the following request to update employee properties based on the employee id:
curl --location --request PUT 'http://localhost:8080/employees/1' --header 'Content-Type: application/json' \
--data-raw '{
"id": 1,
"userName": "john.doe",
"firstName": "John",
"middleName": "Thomas",
"lastName": "Doe",
"emailId": "john.doe7@gmail.com",
"dayOfBirth": "1997-12-03"
}'
view rawupdate-request.json hosted with ❤ by GitHub
We receive the following response in JSON format:
{
"id": 1,
"userName": "john.doe",
"firstName": "John",
"middleName": "Thomas",
"lastName": "Doe",
"emailId": "john.doe7@gmail.com",
"dayOfBirth": "1997-12-03"
}
view rawupdate-response.json hosted with ❤ by GitHub
9.5. Delete an Employee
We send the following request to delete an employee based on the employee id:
curl --location --request DELETE 'http://localhost:8080/employees/1' --header 'Content-Type: application/json'
view rawdelete-request.json hosted with ❤ by GitHub
-
Conclusion
In this tutorial, we looked into creating a microservice application and expose REST API using Kotlin and Spring Boot. In the process, I have also used some best practices which we can utilize while building such applications.All Rights Reserved
Works done by Anirban
Top comments (0)