Creating a TODO application using Akka HTTP and Slick — Chapter 1.
In this tutorial we will be creating a TODO application. The key features of our application are:
- List todo tasks
- List todo tasks for a given date
- Update todo task
- Store todo tasks in PostgreSQL using Slick
This is a simple example, but we can apply these technologies and concepts in more advanced scenarios.
You can find this project on GitHub as well.
Creating a new project
Although I will use IntelliJ IDEA with Scala plugin in this tutorial, you can use your preferred IDE or text editor.
After you started IntelliJ IDEA go to Create New Project
then on the left side choose Scala
and then choose SBT
. On the next screen name our project todo-application
. Then click on Finish
.
Adding libraries to our application
We are going to use the following libraries:
- Akka HTTP for implementing REST endpoints
- Akka HTTP Spray JSON for marshalling and unmarshalling
- Slick to access PostgreSQL
- ScalaGuice for dependency injection
- ScalaLogging for logging
- PostgreSQL JDBC driver to access PostgreSQL
We can add dependencies to our application by editing the build.sbt
, which is located in the root folder of the project.
Let’s add the following lines:
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % "10.0.10",
"com.typesafe.slick" %% "slick" % "3.2.1",
"com.typesafe.akka" %% "akka-http-spray-json" % "10.0.10",
"net.codingwell" %% "scala-guice" % "4.1.0",
"ch.qos.logback" % "logback-classic" % "1.2.3",
"com.typesafe.scala-logging" %% "scala-logging" % "3.7.2",
"org.postgresql" % "postgresql" % "42.1.4"
)
After adding these lines, save build.sbt
. In order for IDEA to be able to recognise which libraries we are using, we have to refresh our project. You can refresh the project by opening the SBT
tool, which is located on the right side of the window and click on the refresh icon.
Creating REST endpoints
As I mentioned earlier we have to create 3 different endpoints to provide information about our tasks. We have to create the following endpoints:
GET /todo?date='date'
— get todo tasks for a given dateGET /todo/
— get all todo tasksPOST /todo/
— create a new todo taskPUT /todo/:id
— set a todo task as done
GET endpoint
We are going to use Akka HTTP to create these endpoints. Akka HTTP has a special, and maybe (at first) weirdly looking DSL which describes our endpoints, but don’t be afraid, we will rock it! Let’s create a new directory in the scala
folder called rest
. After we created this folder, create a new Scala class and name it TodoRoute
.
With the path
keyword we can create a new path. In our case this would be todo
. For this path we have to create 3 different directives (get, post, put). We use the tilde (~
) character to separate the directives under our path.
val route: Route = {
path("todo") {
get {
???
} ~
post {
???
}
} ~
path("todo" / IntNumber) {
put {
???
}
}
}
Now we will implement our GET endpoint. We use the date
parameter to be able to categorise by date. By default Akka reads parameters as String
but we would like to have a Date
. Akka have default Unmarshallers
for example from String
to Int
but not for Date
, so we have to create our custom Unmarshaller
. It is pretty easy:
implicit val dateStringUnmarshaller: Unmarshaller[String, Date] =
Unmarshaller.strict[String, Date] {
import java.text.SimpleDateFormat
val formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
string => formatter.parse(string)
}
The next step is to make thedate
parameter optional. Akka has a custom DSL for this, too. For optional parameters we have to write a question mark (?
) after the name of the parameter. This is how our parameter parsing should look like in the end:
get {
parameter("date".as[Date].?) { (date: Option[Date]) =>
???
}
}
Just to make things clear, this is how we get Option[Date]
:
- Read parameter “date” as a
String
—"date"
- Read parameter “date” as a
String
and convert it toDate
—"date".as[Date]
- Read parameter “date” as a
String
and convert it toDate
and make this parameter optional —"date".as[Date].?
Now we have the GET endpoint, but it does not return any data. We have to create the entity which we would like to send as a response. Let’s create a new directory called entities
in our rest
directory. In entities
create a Todo
class which will be a case class. For the sake of simplicity a todo task will only have 4 properties:
id: Int
name: String
deadline: Date
isDone: Boolean
case class Todo(id: Int, name: String, deadline: Date, isDone: Boolean)
After we created our Todo
entity we should create a (un)marshaller. In order to have a (un)marshaller we create a JsonSupport
trait next to the TodoRoute
class. Before we can carry on with creating the (un)marshaller, we should create a custom JsonFormat
to handle java.util.Date
at parsing. For this I am going to use Owain Lewis’ date marshaller (link). Create an object called DateMarshalling
in the rest
directory, and copy and paste the following code.
package rest.util
import java.text._
import java.util._
import scala.util.Try
import spray.json._
object DateMarshalling {
implicit object DateFormat extends JsonFormat[Date] {
def write(date: Date) = JsString(dateToIsoString(date))
def read(json: JsValue) = json match {
case JsString(rawDate) =>
parseIsoDateString(rawDate)
.fold(deserializationError(s"Expected ISO Date format, got $rawDate"))(identity)
case error => deserializationError(s"Expected JsString, got $error")
}
}
private val localIsoDateFormatter = new ThreadLocal[SimpleDateFormat] {
override def initialValue() = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
}
private def dateToIsoString(date: Date) =
localIsoDateFormatter.get().format(date)
private def parseIsoDateString(date: String): Option[Date] =
Try{ localIsoDateFormatter.get().parse(date) }.toOption
}
Now we should continue implementing the JsonSupport
. Create this trait next to the TodoRoute
class.
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
import rest.util.DateMarshalling._
implicit val todoFormat = jsonFormat4(Todo)
}
As you can see, we are using the jsonFormat4
here. This is because our Todo
case class has 4 properties. If we had 5 properties, we would have had to use jsonFormat5
, it is that simple! Now let’s extend the JsonSupport
in our TodoRoute
class.
class TodoRoute extends JsonSupport
Now, we can send Todo
as a response. In this post we are not going to go deeper, we will just provide a mock response.
val mockTodos = Seq(
Todo(0, "first", new Date(0), isDone = false),
Todo(1, "second", new Date(0), isDone = false),
Todo(2, "third", new Date(0), isDone = false)
)
Now we should complete the request with complete(mockTodos)
.
POST endpoint
This endpoint will be much simpler for now, because we just mock the endpoint. It looks the same as the GET endpoint, however, there is a slight difference because we have to parse the receivied JSON to a Todo
object. Fortunately we have already created the (un)marshaller for the Todo
class, so we just have to use Akka HTTP’s as
method which will find our implicit conversion provided by Spray JSON. After we parsed the entity, we just return 201 Created
.
post {
entity(as[Todo]) { (todo: Todo) =>
complete(StatusCodes.Created)
}
}
PUT endpoint
This endpoint will be used to change an existing todo task to a different one. We are going to use this endpoint to change the state of the todo task from doing to done. We could create a new endpoint for each attribute of the todo task but we are not going to do this here. It is necessary to be able to extract the todo task id from the path, we can do this by path("todo" / IntNumber)
. We just simply return with a 204 No Content
. This is how our path should look like:
path("todo" / IntNumber) { (todoId: Int) =>
put {
entity(as[Todo]) { (todo: Todo) =>
complete(StatusCodes.NoContent)
}
}
}
This is how our TodoRoute
and JsonSupport
should look like in the end.
package rest
import java.util.Date
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.unmarshalling.Unmarshaller
import rest.entities.Todo
import spray.json.DefaultJsonProtocol
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
import rest.util.DateMarshalling._
implicit val todoFormat = jsonFormat4(Todo)
}
class TodoRoute extends JsonSupport {
implicit val dateStringUnmarshaller: Unmarshaller[String, Date] =
Unmarshaller.strict[String, Date] {
import java.text.SimpleDateFormat
val formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
string => formatter.parse(string)
}
val route: Route = {
path("todo") {
get {
parameter("date".as[Date].?) { (date: Option[Date]) =>
val mockTodos = Seq(
Todo(0, "first", new Date(0), isDone = false),
Todo(1, "second", new Date(0), isDone = false),
Todo(2, "third", new Date(0), isDone = false)
)
date match {
case Some(dt) => complete(mockTodos)
case None => complete(mockTodos)
}
}
} ~
post {
entity(as[Todo]) { (todo: Todo) =>
complete(StatusCodes.Created)
}
}
} ~
path("todo" / IntNumber) { (todoId: Int) =>
put {
entity(as[Todo]) { (todo: Todo) =>
complete(StatusCodes.NoContent)
}
}
}
}
}
Create REST server
We create a RESTServer
object in the scala
directory extending App
. We also have to provide an ActorSystem
, an ActorMaterializer
and an ExecutionContext
. For now I did not want to go deeper, but I will do so if there is an interest.
Now we have actors who will handle the HTTP requests, so the only thing left is to make our REST server listen on a specific port.
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import rest.TodoRoute
object RESTServer extends App{
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
implicit val executionContext = system.dispatcher
val route = new TodoRoute().route
Http().bindAndHandle(route, "localhost", 8080)
}
Bottom line
Finally we have a simple REST server, which can handle the specified REST requests. In the next chapter we are going to implement database access, change the configuration settings to use application.conf, add dependency injection, refactor date (un)marshalling and more!
If you find any mistakes, or simply want to get in touch, feel free to contact me.
You can find this project on GitHub as well.