From our previous blog on the basics of http4s, we are familiar with http4s library and what all makes it a go-to solution to develop a Scala based Http service. The library provides out-of-box support for creating http routes, servers and clients. Using this, we can easily develop a self-contained http4s dependent app.
Demo with http4s
In our demo, we will try creating a basic CRUD application using http4s. In this service, we will create multiple endpoints where we will update an in-memory user list by adding a new user to it, delete and update an existing one. We will create the following components:
Http Routes
Since http4s provides us with HttpRoutes
, we can create the following endpoints for our user crud service using a list of case statements:
- List all existing users
case GET -> Root /"users" => Ok(UserService.findAll())
UserService.findAll() is a custom code that lists all the users from our in-memory user list. For an actual implementation, this code will fetch the list of existing users from an actual database.
We can access the above route using the endpoint http://localhost:8080/users
- Get information for an existing user using id
case GET -> Root / "users" / LongVar(id) =>
Ok(UserService.find(id)).handleErrorWith(errorHandler)
This http route can be hit using the endpoint http://localhost:8080/users
/4. The final path segment “/4"
is extracted by LongVar(id)
path extractor and implicitly converted into Long. Again, UserService.find(id)
is a custom code that fetches the required user information from the user list if it exists, else an error is returned to the client.
- Add a new user
case req @ POST -> Root / "users" =>
req.as[UserForm].flatMap { userForm =>
val user = User(Random.nextInt(1000), userForm.username,
userForm.email, userForm.age)
UserService.save(user)
}
Adding a new user is a POST
http request and it can be hit using the endpoint http://localhost:8080/users
and passing the user information to be added as body of the request. In the request handler side, we are parsing the body of the request to a case class UserForm
and storing the received information in the user list.
- Delete an existing user
case DELETE -> Root / "users" / LongVar(id) =>
UserService.remove(id).flatMap(_ =>
NoContent()).handleErrorWith(errorHandler)
To access this route, we will send a DELETE
http request with the endpoint http://localhost:8080/users
/4
- Update user information
case req @ PUT -> Root / "users" / LongVar(id) =>
req.as[UserAgeForm].flatMap { ageForm =>
UserService.edit(id, ageForm.age).flatMap(_ =>
Accepted()).handleErrorWith(errorHandler)
}
We can hit this route by a PUT
http request http://localhost:8080/users
/4 and sending the updated information as part of the request body.
We can combine all these route case statements together as:
val userServiceAPI = HttpRoutes
.of[IO] {
case GET -> Root /"users" => .......
case POST -> Root / "users" => .......
........
}
Http Server
Once the routes are present, we need a server to host these routes. http4s provides support for a bunch of servers out of the box, but we will use ember-server to achieve the same:
EmberServerBuilder .default[IO] .withHost(Host.fromString(host).get) .withPort(Port.fromInt(port).get) .withHttpApp(userServiceAPI) // Passing the created routes to server .build
Http Client
Like server, http4s supports ember-client to create http clients as well. We can create a client using ember-client and hit each of the endpoints created above.
EmberClientBuilder.default[IO].build
.use { client =>
client.expect[String](http://localhost:8080/users/4")
}
Apart from a simple GET Http method, we can create the following requests as well:
POST
method
val newUser = json"""{"username":"John Doe", "email":"john.doe@gmail.com", "age":29}"""
val postRequest: Request[IO] = Request[IO](
method = Method.POST,
uri = Uri.unsafeFromString("http://localhost:8080/users")
).withEntity(newUser)
client.expect[String](newUserReq)
PUT
method
val updatedAgeForExisting = json"""{"age":65}"""
def putRequest(userId: String): Request[IO] = Request[IO](
method = Method.PUT,
uri = Uri.unsafeFromString("http://localhost:8080/users/$userId")
).withEntity(updatedAgeForExisting)
client.expect[String](putRequest("4"))
DELETE
method
def deleteRequest(userId: String): Request[IO] = Request[IO](
method = Method.DELETE,
uri = Uri.unsafeFromString(s"http://localhost:8080/users/$userId")
)
client.expect[String](deleteRequest("4"))
Conclusion
Now, we have all the different components of an http app ready and just needs stitching together. The complete code with the correct wiring is present here. This is a very basic http service, with no authentication and authorisation involved. But http4s provides different middleware services as well that we can use to make your code more secure. The details of the same are here.