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 tasks for a given date GET /todo/ — get all todo tasks

— get all todo tasks POST /todo/ — create a new todo task

— 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 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 the date 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 to Date — "date".as[Date] 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