Creating a TODO application using Akka HTTP and Slick — Chapter 1.

Andras Zsamboki
samebug
Published in
7 min readOct 17, 2017

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.

New project

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.

build.sbt

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 date
  • GET /todo/ — get all todo tasks
  • POST /todo/ — create a new todo task
  • PUT /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 Intbut 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]:

  1. Read parameter “date” as a String "date"
  2. Read parameter “date” as a String and convert it to Date"date".as[Date]
  3. Read parameter “date” as a String and convert it to Date 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.

Sources

Akka HTTP
Owain Lewis

--

--