Scala Rest API with akka-http

Page content

Introduction

In this tutorial we shall explore how we can use akka-http to create service with scala. Here we create CRUD service with akka actor and try to see how it work.

Versions

  • Scala version: 2.13.1
  • SBT version: 1.3.4

Other dependencies

scalaVersion := "2.13.1"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.5.26",
"com.typesafe.akka" %% "akka-http" % "10.1.11",
"com.typesafe.akka" %% "akka-stream" % "2.5.26",
"com.typesafe.akka" %% "akka-http-spray-json" % "10.1.11",
)
view raw build.sbt hosted with ❤ by GitHub

About the service/project

We’ll create two actors that communicate with each other to get the user data and user activity data.

output of the application

Data Classes

We need two data classes that keeps the user data and user activity data.

// holds user data
case class UserData(data: String)
// holds user activity data
case class UserActitity(activity: String)
view raw UserData.scala hosted with ❤ by GitHub

Repository

Repositories are used to fetch the data from the actual source of data. The actual source can be anything like another service or some database. We created UserActivityRepository that have only one method queryHistoricalActivities to fetch the user activity by userId.

import scala.concurrent.Future
trait UserActivityRepository {
def queryHistoricalActivities(userId: String):
Future[List[UserActivity]]
}

Actors to get user data

We need two actor classes for UserDataActor and UserActivityActor. UserDataActor received 4 types of methods Get, Post,Put and Delete to retrieve, create, update and delete the user data respectively. Once a message received, User data will be sent to the sender asynchronously.

object UserDataActor {
case object Get
case object Post
case object Put
case object Delete
}
//Backend Service
class UserDataActor extends Actor {
import UserDataActor._
override def receive: Receive = {
case Get =>
sender() ! UserData("data Searched")
case Post =>
sender() ! UserData("data created!")
case Put =>
sender () ! UserData("data updated!")
case Delete =>
sender () ! UserData("data deleted!")
}
}

UserActivityActor retrieve the user active once it receives the message Get

object UserActivityActor {
case object Get
}
class UserActivityActor(val userId: String,
implicit val repository: UserActivityRepository)
extends Actor {
implicit val ec: ExecutionContextExecutor = context.dispatcher
override def receive: Receive = {
case Get =>
repository.queryHistoricalActivities(userId) pipeTo sender
}
}

Routing Configuration

Now, we need to create routing details, to access the user details from rest endpoints. We needs following endpoints

PathDetail
GET /api/users/activityRetrieve user and its activity details.
POST /api/userTo create user data
PUT /api/userTo update user data
DELETE /api/userTo delete user data

Routing code looks like below -

class RouteConfig(implicit val userDataActorRef: ActorRef,
implicit val system: ActorSystem) {
val timeoutMills: Long = 2 * 1000
val getRoute: Route =
PathDirectives.pathPrefix("user"){
path("activity") {
get {
val userData = findData(UserDataActor.Get)
val userActivityActorRef: ActorRef =
system.actorOf(Props(new UserActivityActor(userData.data, new UserActivityRepositoryImpl())))
val activity: UserActivity = findUserActivityData(userActivityActorRef)
RouteDirectives.complete(HttpEntity(activity.toString))
}
}
}
private def findUserActivityData(userActivityActorRef: ActorRef) = {
val resultFuture = Patterns.ask(userActivityActorRef, UserActivityActor.Get, timeoutMills)
val result: List[UserActivity] = Await.result(resultFuture, Duration.create(2, TimeUnit.SECONDS)).asInstanceOf[List[UserActivity]]
val activity: UserActivity = result.head
activity
}
val postRoute: Route = path("user") {
post {
//TODO: DO SOME OPERATION TO SAVE USER DATA
executeActorAndSearchData(UserDataActor.Post)
}
}
val deleteRoute: Route = path("user") {
delete {
//TODO: DO SOME OPERATION TO DELETE USER DATA
executeActorAndSearchData(UserDataActor.Delete)
}
}
val putRoute: Route = path("user") {
put {
//TODO: DO SOME OPERATION TO UPDATE USER DATA
executeActorAndSearchData(UserDataActor.Put)
}
}
val executeActorAndSearchData: Any => StandardRoute = (message: Any) => {
val result: UserData = findData(message)
RouteDirectives.complete(HttpEntity(result.data))
}
private def findData(message: Any) = {
val resultFuture = Patterns.ask(userDataActorRef, message, timeoutMillis = timeoutMills)
val result: UserData = Await.result(resultFuture, Duration.create(2, TimeUnit.SECONDS)).asInstanceOf[UserData]
result
}
}

We have two implicit parameters in the routing config implicit val userDataActorRef: ActorRef and implicit val system: ActorSystem. userDataActorRef is used to get the user data. Since user details is required to register UserActivityActor we use actor system and implicit parameter to register UserActivityActor when we have user information available.

Web Server

This class is used to create http server and bind the endpoints with the http server.

package org
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.directives.PathDirectives.pathPrefix
import akka.stream.ActorMaterializer
import org.user.actor.UserDataActor
import org.user.repositories.UserActivityRepositoryImpl
import scala.concurrent.ExecutionContextExecutor
import scala.io.StdIn
object WebServer extends App {
implicit val system: ActorSystem = ActorSystem("web-app")
private implicit val dispatcher: ExecutionContextExecutor = system.dispatcher
private implicit val materialize: ActorMaterializer = ActorMaterializer()
implicit val userActivityRepo = new UserActivityRepositoryImpl()
implicit val userDataActorRef: ActorRef = system.actorOf(Props(new UserDataActor()))
private val routeConfig = new RouteConfig()
val routes = {
pathPrefix("api") {
concat(
routeConfig.getRoute,
routeConfig.postRoute,
routeConfig.deleteRoute,
routeConfig.putRoute
)
}
}
val serverFuture = Http().bindAndHandle(routes, "localhost", 8080)
println("Server started ...")
StdIn.readLine()
serverFuture
.flatMap(_.unbind())
.onComplete(_ => system.terminate())
view raw WebServer.scala hosted with ❤ by GitHub

Run Application

Compile the code using below command

sbt compile

Run the application

sbt run 

Now go to terminal, and run below httpie scripts output of the application

Github links

Full code is available here to explore and fork. Feel free to do whatever you want. ;)

Conclusion

We have seen here it’s easy to create rest services with akka and scala. You can get more information at akka-http official document.