1. Introduction

Akka HTTP is one of the most popular HTTP toolkits in Scala. It’s built on top of Akka and uses Actors under the hood. Play Framework uses Akka HTTP for its REST module and route handling.

In this tutorial, let’s create a basic REST application using Akka HTTP.

2. Setup

Let’s add the required dependencies for Akka-HTTP to build.sbt:

libraryDependencies ++= Seq( 
  "com.typesafe.akka" %% "akka-http" % "10.4.0",
  "com.typesafe.akka" %% "akka-actor-typed" % "2.7.0",
  "com.typesafe.akka" %% "akka-http-spray-json" % "10.4.0"
)

Akka HTTP expects that the Akka module is available on the classpath. Hence, we need to provide the required version of the Akka library explicitly.

Additionally, for this tutorial, we have added the akka-http-spray-json module to handle JSON serialization and deserialization.

Moreover, we can use cURL or Postman for testing the HTTP endpoints with ease.

3. Simple Akka-HTTP Server

Let’s create a very simple REST server using Akka-HTTP.

3.1. Defining the First Route

A route is essentially a mapping between an input request and an output response. Let’s define our first Route. We can create a route by providing the REST API resource in the path directive:

val route: Route = path("movies" / "heartbeat") {
  get {
    complete("Success")
  }
}

The get directive within path lets Akka HTTP know that we’re expecting a GET request. We can then send a response back using the method complete().

We can also skip adding the get directive since Akka HTTP automatically maps this route to the GET request. As a result, we can re-write the above code block:

val route: Route = path("movies" / "heartbeat") { 
  complete("Success") 
}

3.2. Creating the Server

Now that the route is ready, let’s create the server. Since it uses Akka actors under the hood, we need to create an actor system and provide it as an implicit value:

implicit val system = ActorSystem("MoviesServer")

Next, we can instantiate a server instance and bind it to a port and the previously created route:

val server = Http().newServerAt("localhost", 9090).bind(route)
server.map { _ =>
  println("Successfully started on localhost:9090 ")
} recover {
  case ex =>
    println("Failed to start the server due to: "+ex.getMessage)
}

Here, we’re creating an HTTP server at localhost:9090. We need to bind all the REST endpoints to this server instance for it to be accessible.

3.3. Running the App

Finally, we can wire up all the components together and create a running app:

object MovieServer extends App {
  implicit val system = ActorSystem("MoviesServer")
  val route = path("movies" / "heartbeat") {
    get {
      complete("Success")
    }
  }
  val server = Http().newServerAt("localhost", 9090).bind(route)
  server.map { _ =>
    println("Successfully started on localhost:9090 ")
  } recover { case ex =>
    println("Failed to start the server due to: " + ex.getMessage)
  }
}

Now, let’s start the application and access the URL http://localhost:9090/movies/heartbeat. This should respond with the message Success if everything’s fine.

4. Concatenate Multiple Routes

We can concatenate multiple routes using the ~ operator. Let’s add one more route to the above example:

path("movies" / "heartbeat") {
  get {
    complete("Success")
  }
} ~ path("movies" / "test") {
  get {
    complete("Verified")
  }
}

Now, when we re-run the application, we can access both endpoints using their relevant paths.

5. JSON Handling

Nowadays, JSON is considered the de-facto standard in REST applications for serialization. Now that our basic application is working fine, let’s look at the JSON configurations needed for serializing and deserializing the data. Akka HTTP has official integration with spray-json using the akka-http-spray-json module.

We should provide implicit converters for each of the case classes. Let’s create a case class and provide the implicit format for using it in our application:

case class Movie(id:Int, name:String, length: Int)
import spray.json.DefaultJsonProtocol._
object JsonImplicits {
  implicit val movieFormat = jsonFormat3(Movie)
}

We need to use the correct method based on the number of fields of the case class. The method name follows the pattern jsonFormatX, where X is the number of fields in the case class. Since the Movie case class has three fields, we use the method jsonFormat3(). Akka HTTP uses this implicit format to serialize and deserialize the request body and response.

6. CRUD Operations

In this section, let’s look at how we implement CRUD operations using Akka HTTP.

6.1. GET Method

As we saw before, we can use the get directive to create a route that accepts GET requests. Let’s create an endpoint that returns all the movies in our database:

path("movies-sync") {
  get {
    complete(movieService.getAllMoviesSync())
  }
} 

The method getAllMoviesSync() returns the movies as List[Movie]. Akka HTTP is able to serialize this result to JSON as movieFormat is available in the implicit scope.

Now, let’s see how we can handle async responses. For that, we need to use the onComplete() method on the Future results:

path("movies") {
  get {
    onComplete(movieService.getAllMovies()) {
      case Success(res) => complete(res)
      case Failure(ex)  => complete(StatusCodes.InternalServerError)
    }
  }
}

6.2. POST Method

Now let’s add a POST method to save the movie into the database. Let’s create the route block for POST:

path("movies") {
  post {
    entity(as[Movie]) { movie =>
      onComplete(movieService.saveMovie(movie)) {
        case Success(res) => complete(res)
        case Failure(ex)  => complete(StatusCodes.InternalServerError)
      }
    }
  }
}

The movie JSON is sent in the body of the POST request. The method entity() extracts the JSON body. We can deserialize the JSON to the Movie case class using the method as. However, we should make sure that the implicit format for the Movie case class is available in the scope.

We can test this method using a curl request:

curl -X POST http://localhost:9090/movies -d '{"id": 1, "name": "The Prestige", "length":130}' -H 'Content-Type: application/json'

We can verify that the movie is saved successfully by executing the previous GET request in the browser:

http://localhost:9090/movies

6.3. PUT Request

Next, let’s implement a PUT request to update an existing movie. We can use the put directive for building the service:

path("movies" / IntNumber) { id =>
  put {
    entity(as[Movie]) { movie =>
      onComplete(movieService.updateMovie(id, movie)) {
        case Success(res) => complete(res)
        case Failure(ex)  => complete(StatusCodes.InternalServerError)
      }
    }
  }
}

Here, we used a special type IntNumber within path. This extracts the integer value passed in the URL. In this case, we’re using id of the movie to be updated in the URL.

Let’s try to update the previously inserted movie using a curl request:

curl -X PUT http://localhost:9090/movies/1 -d '{"id":1, "name": "The Prestige", "length":125}'  -H 'Content-Type: application/json'

This updates the length of the movie from 120 to 125.

6.4. DELETE Request

We can use the delete directive to implement the DELETE request. Let’s implement the route to delete a movie from the database:

path("movies" / IntNumber) { id =>
  delete {
    onComplete(movieService.deleteMovie(id)) {
      case Success(res) => complete(res)
      case Failure(ex)  =>  complete(StatusCodes.InternalServerError)
    }
  }
}

Now, we can invoke the endpoint using curl:

curl -X DELETE http://localhost:9090/movies/1

7. Handling Request and Response Headers

In any REST application, request and response headers play a very important role. Let’s look at how we can handle them using Akka HTTP.

We can create a simple GET endpoint that parses a request header and responds with another header:

path("test-headers") {
  get {
    headerValueByName("apiKey") { apiKey =>
      respondWithHeader(RawHeader("authenticated", "done")) {
          complete(s"Received apiKey in header as '$apiKey' ")
      }
    }
  }
}

The directive headerValueByName extracts the header with the name apiKey. If the header is optional, we can use the directive optionalHeaderValueByName instead.

Similarly, the directive respondWithHeader adds the provided header to the response. Let’s test this with a simple curl request:

curl -v -X GET http://localhost:9090/test-headers -H 'Content-Type: application/json' -H 'apiKey: my-api-key'

We can see the request and response headers in the curl output:

Akka HTTP Headers

8. Conclusion

In this article, we looked at Akka HTTP and how we can build a simple REST application using it. We discussed how to create a CRUD application and how to combine multiple routes together. Finally, we also looked at how to integrate Spray JSON with Akka HTTP.

As always, the sample code used in this tutorial is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments