Building a Simple REST API with Scala, Akka Actor & Http

Prerequisites:

  • Scala
  • sbt

Setting up the Project:

Initial Project Structure
ThisBuild / version := "0.1.0-SNAPSHOT"

ThisBuild / scalaVersion := "2.13.8"

lazy val root = (project in file("."))
.settings(
name := "AnimeQuotes",
version := "0.1",
scalaVersion := "2.13.8",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.6.19",
"com.typesafe.akka" %% "akka-http-core" % "10.2.9",
"com.typesafe.akka" %% "akka-http" % "10.2.9",
"com.typesafe.akka" % "akka-actor-typed_2.13" % "2.6.19",
"com.typesafe.akka" % "akka-stream-typed_2.13" % "2.6.19",
"com.typesafe.akka" % "akka-http-spray-json_2.13" % "10.2.9")
)
Sbt compile
  • From the Run menu, select Edit configurations
  • Click the + button and select sbt Task.
  • Name it Run
  • In the Tasks field, type ~run. The ~ causes sbt to rebuild and rerun the project when you save changes to a file in the project.
  • Click OK.
  • Click on the Run button to run the application.

Rest API:

/api/anime/random ~> use animeChan API to fetch & return random anime quote/api/favorite/all ~> returns a list of all favorite anime quotes/api/favorite/add ~> add anime quotes to the favorite list/api/favorite/delete ~> removes anime quotes from the favorite list

Building a Simple Server

  1. Actor Model
  2. Marshalling & Unmarshalling
  3. Future & Await
  • send a finite number of messages to other actors;
  • create a finite number of new actors;
  • designate the behavior to be used for the next message it receives.
Actor Architecture
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import scala.concurrent.ExecutionContextExecutor
import scala.util.{Failure, Success}

object AnimeQuoteServer extends App {
def run(): Unit = {
implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "AnimeQuoteWebServer")
implicit val executionContext: ExecutionContextExecutor = system.executionContext
val host = "localhost"
val port = 9000

val binding = Http().newServerAt(host, port).bindFlow(route)
binding.onComplete {
case Success(_) =>
println(s"Server is listening on http://$host:$port")
case Failure(exception) =>
println(s"Failure : $exception")
system.terminate()
}
}
}
val animeRoutes = pathPrefix("anime") {
get {
path("random") {
complete(s"Random Route")
}
}
}

val route = pathPrefix("api") {
animeRoutes
}
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import scala.concurrent.ExecutionContextExecutor
import scala.util.{Failure, Success}

object AnimeQuoteServer extends App {
def run(): Unit = {
implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "AnimeQuoteWebServer")
implicit val executionContext: ExecutionContextExecutor = system.executionContext
val host = "localhost"
val port = 9000

val animeRoutes = pathPrefix("anime") {
get {
path("random") {
complete(s"Random Route")
}
}
}

val route = pathPrefix("api") {
animeRoutes
}

val binding = Http().newServerAt(host, port).bindFlow(route)
binding.onComplete {
case Success(_) =>
println(s"Server is listening on http://$host:$port")
case Failure(exception) =>
println(s"Failure : $exception")
system.terminate()
}
}
}
Initial Get Random Anime Quote Request

Enhancing the Get Request:

  • Marshalling & Unmarshalling
  • Future & Await
Enhanced Get Request Folder Structure
package model

case class AnimeQuote(anime: String, character: String, quote: String)
case class AnimeQuotes(data:Array[AnimeQuote])
package util

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import model._
import spray.json._

trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val animeQuoteFormat: RootJsonFormat[AnimeQuote] = jsonFormat3(AnimeQuote)
implicit val animeQuotesFormat: RootJsonFormat[AnimeQuotes] = jsonFormat1(AnimeQuotes)
}
package external

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors

import scala.concurrent.ExecutionContextExecutor

trait API {
implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "SingleRequest")
implicit val executionContext: ExecutionContextExecutor = system.executionContext
val endpoint=""
}
package external

import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
import akka.http.scaladsl.unmarshalling.Unmarshal
import model._
import util.JsonSupport

import scala.concurrent.duration._
import scala.concurrent.{Await, Future}

object AnimeChanApi extends API with JsonSupport {
override val endpoint = "https://animechan.vercel.app/api"

def getRandomAnimeQuote(): AnimeQuote = {
val responseFuture: Future[HttpResponse] = Http().singleRequest(HttpRequest(uri = s"$endpoint/random"))
val ftRes = Await.result(responseFuture, 2.second)
val resFuture = Unmarshal(ftRes.entity).to[AnimeQuote]
val animeQuote = Await.result(resFuture, 2.second)
animeQuote
}

}
package handler

import external.AnimeChanApi
import model.AnimeQuote

object ServerHandler {

def getRandomAnimeQuotes(): AnimeQuote = {
AnimeChanApi.getRandomAnimeQuote()
}

}
  • Extend AnimeQuoteServer with JsonSupport
object AnimeQuoteServer extends App with JsonSupport 
  • Change AnimeRoute’s response
val animeRoutes = pathPrefix("anime") {
get {
path("random") {
complete(ServerHandler.getRandomAnimeQuotes())
}
}
}
Get Random Anime Quote Request

Extending to CRUD Operations:

/api/favorite/all /api/favorite/add /api/favorite/delete
package database

import model.AnimeQuote

object Database {
var favoriteQuotes: Array[AnimeQuote] = Array()
}
def getFavoriteAnimeQuotes(): Array[AnimeQuote] = {
Database.favoriteQuotes
}

def addAnimeQuotesToFavorite(animeQuote: AnimeQuote): String = {
if (Database.favoriteQuotes.contains(animeQuote)) {
"AnimeQuote Already In"
}
else {
Database.favoriteQuotes = Database.favoriteQuotes :+ animeQuote
"AnimeQuote Added Successfully"
}
}

def deleteAnimeQuotesToFavorite(animeQuote: AnimeQuote): String = {
if (Database.favoriteQuotes.contains(animeQuote)) {
Database.favoriteQuotes = Database.favoriteQuotes.filter(_.quote != animeQuote.quote)
"AnimeQuote Removed Successfully"
}
else {
"AnimeQuote Not Found"
}
}
val favoriteRoute = pathPrefix("favorite") {
concat(
get {
path("all") {
complete(ServerHandler.getFavoriteAnimeQuotes())
}
},
post {
path("add") {
entity(as[AnimeQuote]) { animeQuote =>
complete(ServerHandler.addAnimeQuotesToFavorite(animeQuote))
}
}
},
delete {
path("delete") {
entity(as[AnimeQuote]) { animeQuote =>
complete(ServerHandler.deleteAnimeQuotesToFavorite(animeQuote))
}
}
}
)
}

val route = pathPrefix("api") {
concat(animeRoutes, favoriteRoute)
}
Adding Anime Quote to Favorite list
Accessing Favorites list
Removing Anime Quote from Favorite list

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Utsav Khatu

Utsav Khatu

Former Technology Analyst Intern @Morgan-Stanley | Full-Stack Web Developer | Machine Learning Enthusiast | VJTI ‘23