Scala Ecosystem and Popular Libraries: Building Real-World Applications
Introduction
The Scala ecosystem is rich with high-quality libraries and frameworks that enable you to build everything from web services to big data processing systems. Understanding the ecosystem and knowing which libraries to use for different use cases is crucial for productive Scala development.
This lesson provides a comprehensive tour of the most important Scala libraries and frameworks, showing you how to integrate them effectively and make informed decisions about which tools to use for your projects.
Core Infrastructure Libraries
SBT (Scala Build Tool)
// build.sbt - Essential SBT configuration
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.13.10"
lazy val root = (project in file("."))
.settings(
name := "my-scala-app",
// Compiler options
scalacOptions ++= Seq(
"-deprecation",
"-encoding", "UTF-8",
"-feature",
"-unchecked",
"-Xlint",
"-Ywarn-dead-code",
"-Ywarn-numeric-widen",
"-Ywarn-value-discard"
),
// Dependencies
libraryDependencies ++= Seq(
// Testing
"org.scalatest" %% "scalatest" % "3.2.15" % Test,
"org.scalatestplus" %% "scalacheck-1-17" % "3.2.15.0" % Test,
// JSON handling
"io.circe" %% "circe-core" % "0.14.5",
"io.circe" %% "circe-generic" % "0.14.5",
"io.circe" %% "circe-parser" % "0.14.5",
// HTTP client
"com.softwaremill.sttp.client3" %% "core" % "3.8.13",
"com.softwaremill.sttp.client3" %% "circe" % "3.8.13",
// Logging
"ch.qos.logback" % "logback-classic" % "1.4.6",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",
// Configuration
"com.typesafe" % "config" % "1.4.2",
// Cats for functional programming
"org.typelevel" %% "cats-core" % "2.9.0",
"org.typelevel" %% "cats-effect" % "3.4.8"
),
// Test configuration
Test / parallelExecution := false,
Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oD"),
// Assembly plugin for fat JARs
assembly / assemblyMergeStrategy := {
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case x => MergeStrategy.first
}
)
// project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.13")
// Example application structure
object BuildUtils {
// Custom SBT tasks
val generateBuildInfo = taskKey[Unit]("Generate build information")
val generateBuildInfoTask = generateBuildInfo := {
val file = (Compile / sourceManaged).value / "BuildInfo.scala"
val version = (ThisBuild / version).value
val scalaVersion = (ThisBuild / scalaVersion).value
val buildTime = java.time.Instant.now().toString
val content = s"""
|object BuildInfo {
| val version: String = "$version"
| val scalaVersion: String = "$scalaVersion"
| val buildTime: String = "$buildTime"
|}
|""".stripMargin
IO.write(file, content)
println(s"Generated BuildInfo at $file")
}
}
// Multi-module build example
lazy val common = (project in file("modules/common"))
.settings(
name := "common",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.9.0"
)
)
lazy val core = (project in file("modules/core"))
.settings(
name := "core"
)
.dependsOn(common)
lazy val api = (project in file("modules/api"))
.settings(
name := "api",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % "10.5.0",
"com.typesafe.akka" %% "akka-stream" % "2.8.0"
)
)
.dependsOn(core)
lazy val web = (project in file("modules/web"))
.settings(
name := "web",
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play" % "2.8.19"
)
)
.dependsOn(core)
Configuration Management
// Using Typesafe Config
import com.typesafe.config.{Config, ConfigFactory}
// application.conf
"""
app {
name = "MyScalaApp"
version = "1.0.0"
database {
url = "jdbc:postgresql://localhost:5432/mydb"
url = ${?DATABASE_URL} # Override with environment variable
username = "user"
username = ${?DB_USERNAME}
password = "password"
password = ${?DB_PASSWORD}
pool-size = 10
}
server {
host = "0.0.0.0"
port = 8080
port = ${?PORT}
}
features {
enable-metrics = true
enable-caching = false
max-connections = 100
}
}
"""
case class DatabaseConfig(
url: String,
username: String,
password: String,
poolSize: Int
)
case class ServerConfig(
host: String,
port: Int
)
case class FeaturesConfig(
enableMetrics: Boolean,
enableCaching: Boolean,
maxConnections: Int
)
case class AppConfig(
name: String,
version: String,
database: DatabaseConfig,
server: ServerConfig,
features: FeaturesConfig
)
object ConfigLoader {
def load(): AppConfig = {
val config: Config = ConfigFactory.load()
val appConfig = config.getConfig("app")
val dbConfig = appConfig.getConfig("database")
val serverConfig = appConfig.getConfig("server")
val featuresConfig = appConfig.getConfig("features")
AppConfig(
name = appConfig.getString("name"),
version = appConfig.getString("version"),
database = DatabaseConfig(
url = dbConfig.getString("url"),
username = dbConfig.getString("username"),
password = dbConfig.getString("password"),
poolSize = dbConfig.getInt("pool-size")
),
server = ServerConfig(
host = serverConfig.getString("host"),
port = serverConfig.getInt("port")
),
features = FeaturesConfig(
enableMetrics = featuresConfig.getBoolean("enable-metrics"),
enableCaching = featuresConfig.getBoolean("enable-caching"),
maxConnections = featuresConfig.getInt("max-connections")
)
)
}
// Configuration with validation
def loadWithValidation(): Either[String, AppConfig] = {
try {
val config = load()
// Validate configuration
val errors = scala.collection.mutable.ListBuffer[String]()
if (config.name.isEmpty) errors += "App name cannot be empty"
if (config.database.url.isEmpty) errors += "Database URL cannot be empty"
if (config.server.port < 1 || config.server.port > 65535) errors += "Invalid port number"
if (config.features.maxConnections <= 0) errors += "Max connections must be positive"
if (errors.nonEmpty) {
Left(s"Configuration errors: ${errors.mkString(", ")}")
} else {
Right(config)
}
} catch {
case e: Exception => Left(s"Failed to load configuration: ${e.getMessage}")
}
}
}
// Usage
val config = ConfigLoader.loadWithValidation() match {
case Right(cfg) =>
println(s"Configuration loaded successfully: ${cfg.name} v${cfg.version}")
println(s"Server will run on ${cfg.server.host}:${cfg.server.port}")
cfg
case Left(error) =>
println(s"Configuration error: $error")
sys.exit(1)
}
// Environment-specific configurations
object Environment extends Enumeration {
val Development, Staging, Production = Value
def current: Environment.Value = {
sys.env.get("ENVIRONMENT").map(_.toLowerCase) match {
case Some("development") => Development
case Some("staging") => Staging
case Some("production") => Production
case _ => Development
}
}
}
def loadEnvironmentConfig(): AppConfig = {
val baseConfig = ConfigFactory.load()
val envConfig = Environment.current match {
case Environment.Development => ConfigFactory.load("development.conf")
case Environment.Staging => ConfigFactory.load("staging.conf")
case Environment.Production => ConfigFactory.load("production.conf")
}
// Layer environment-specific config over base config
val config = envConfig.withFallback(baseConfig)
println(s"Loaded configuration for ${Environment.current} environment")
// Extract configuration using the layered config
ConfigLoader.load() // Would use the layered config in real implementation
}
// Configuration with type safety using PureConfig
import pureconfig._
import pureconfig.generic.auto._
case class TypeSafeConfig(
database: DatabaseConfig,
server: ServerConfig,
features: FeaturesConfig
)
def loadTypeSafeConfig(): Either[ConfigReaderFailures, TypeSafeConfig] = {
ConfigSource.default.at("app").load[TypeSafeConfig]
}
// Usage with better error handling
loadTypeSafeConfig() match {
case Right(cfg) => println(s"Type-safe config loaded: $cfg")
case Left(failures) =>
println("Configuration failures:")
failures.toList.foreach(failure => println(s" - ${failure.description}"))
}
Functional Programming Libraries
Cats - Functional Programming Abstractions
import cats._
import cats.data._
import cats.implicits._
// Basic type class usage
def combineAll[A](list: List[A])(implicit monoid: Monoid[A]): A = {
list.foldLeft(monoid.empty)(monoid.combine)
}
println(combineAll(List(1, 2, 3, 4, 5))) // Uses Int Monoid
println(combineAll(List("hello", " ", "world"))) // Uses String Monoid
println(combineAll(List(List(1, 2), List(3, 4), List(5)))) // Uses List Monoid
// Validated for error accumulation
case class PersonValidation(name: String, age: Int, email: String)
type ValidationResult[A] = ValidatedNel[String, A]
def validateName(name: String): ValidationResult[String] = {
if (name.nonEmpty && name.length >= 2) name.validNel
else "Name must be at least 2 characters".invalidNel
}
def validateAge(age: Int): ValidationResult[Int] = {
if (age >= 0 && age <= 150) age.validNel
else "Age must be between 0 and 150".invalidNel
}
def validateEmail(email: String): ValidationResult[String] = {
if (email.contains("@") && email.contains(".")) email.validNel
else "Email must be valid".invalidNel
}
def validatePerson(name: String, age: Int, email: String): ValidationResult[PersonValidation] = {
(
validateName(name),
validateAge(age),
validateEmail(email)
).mapN(PersonValidation)
}
// Test validation
val validPerson = validatePerson("John Doe", 30, "john@example.com")
val invalidPerson = validatePerson("", -5, "invalid-email")
validPerson match {
case Valid(person) => println(s"✓ Valid person: $person")
case Invalid(errors) => println(s"✗ Validation errors: ${errors.toList}")
}
invalidPerson match {
case Valid(person) => println(s"✓ Valid person: $person")
case Invalid(errors) =>
println("✗ Validation errors:")
errors.toList.foreach(error => println(s" - $error"))
}
// Either with error handling
def divide(a: Double, b: Double): Either[String, Double] = {
if (b == 0) Left("Division by zero")
else Right(a / b)
}
def sqrt(x: Double): Either[String, Double] = {
if (x < 0) Left("Cannot take square root of negative number")
else Right(math.sqrt(x))
}
// Chain operations with for-comprehension
def complexCalculation(a: Double, b: Double, c: Double): Either[String, Double] = {
for {
divided <- divide(a, b)
added = divided + c
sqrtResult <- sqrt(added)
} yield sqrtResult
}
println(complexCalculation(10, 2, 9)) // Right(4.0)
println(complexCalculation(10, 0, 9)) // Left(Division by zero)
println(complexCalculation(10, 2, -20)) // Left(Cannot take square root of negative number)
// StateT for stateful computations
import cats.data.StateT
type GameState = StateT[Id, Int, Int] // State[Int, Int] representing score
def addPoints(points: Int): GameState = StateT { currentScore =>
val newScore = currentScore + points
(newScore, points)
}
def multiplyScore(factor: Int): GameState = StateT { currentScore =>
val newScore = currentScore * factor
(newScore, newScore)
}
def bonusRound: GameState = StateT { currentScore =>
val bonus = if (currentScore > 100) 50 else 10
val newScore = currentScore + bonus
(newScore, bonus)
}
// Compose stateful operations
val gameSequence: GameState = for {
points1 <- addPoints(25)
points2 <- addPoints(30)
multiplied <- multiplyScore(2)
bonus <- bonusRound
} yield bonus
val (finalScore, lastBonus) = gameSequence.run(10) // Starting with score 10
println(s"Final score: $finalScore, Last bonus: $lastBonus")
// Reader for dependency injection
import cats.data.Reader
case class DatabaseConfig(url: String, maxConnections: Int)
case class ApiConfig(endpoint: String, timeout: Int)
case class AppEnvironment(dbConfig: DatabaseConfig, apiConfig: ApiConfig)
def createConnection: Reader[DatabaseConfig, String] = Reader { config =>
s"Connected to ${config.url} with ${config.maxConnections} max connections"
}
def callApi: Reader[ApiConfig, String] = Reader { config =>
s"Called API at ${config.endpoint} with ${config.timeout}ms timeout"
}
def initializeApp: Reader[AppEnvironment, String] = for {
dbConnection <- createConnection.local[AppEnvironment](_.dbConfig)
apiResponse <- callApi.local[AppEnvironment](_.apiConfig)
} yield s"App initialized: $dbConnection, $apiResponse"
val environment = AppEnvironment(
DatabaseConfig("postgresql://localhost:5432/app", 20),
ApiConfig("https://api.service.com", 5000)
)
val appInitResult = initializeApp.run(environment)
println(appInitResult)
// Writer for logging
import cats.data.Writer
type Logged[A] = Writer[List[String], A]
def factorial(n: Int): Logged[Int] = {
if (n <= 1) {
Writer(List(s"Base case: factorial($n) = 1"), 1)
} else {
for {
prev <- factorial(n - 1)
result = n * prev
_ <- Writer.tell(List(s"Calculated: factorial($n) = $n * $prev = $result"))
} yield result
}
}
val (logs, result) = factorial(5).run
println(s"Result: $result")
println("Computation log:")
logs.foreach(log => println(s" $log"))
// Kleisli for composing monadic functions
import cats.data.Kleisli
import cats.instances.option._
type OptionKleisli[A, B] = Kleisli[Option, A, B]
def parseIntSafely: OptionKleisli[String, Int] = Kleisli { str =>
try Some(str.toInt) catch { case _: NumberFormatException => None }
}
def isPositive: OptionKleisli[Int, Int] = Kleisli { n =>
if (n > 0) Some(n) else None
}
def double: OptionKleisli[Int, Int] = Kleisli { n =>
Some(n * 2)
}
// Compose the operations
val parseAndProcess: OptionKleisli[String, Int] =
parseIntSafely andThen isPositive andThen double
println(parseAndProcess.run("42")) // Some(84)
println(parseAndProcess.run("-5")) // None (negative)
println(parseAndProcess.run("abc")) // None (not a number)
// Custom type class instances
case class Money(amount: BigDecimal, currency: String)
implicit val moneyMonoid: Monoid[Money] = new Monoid[Money] {
def empty: Money = Money(BigDecimal(0), "USD")
def combine(x: Money, y: Money): Money = {
require(x.currency == y.currency, "Cannot combine different currencies")
Money(x.amount + y.amount, x.currency)
}
}
implicit val moneyShow: Show[Money] = Show.show { money =>
f"${money.amount}%.2f ${money.currency}"
}
val purchases = List(
Money(BigDecimal("19.99"), "USD"),
Money(BigDecimal("25.50"), "USD"),
Money(BigDecimal("8.75"), "USD")
)
val total = combineAll(purchases)
println(s"Total: ${total.show}")
// Traverse for working with collections
def parseAllInts(strings: List[String]): Option[List[Int]] = {
strings.traverse(parseIntSafely.run)
}
val validNumbers = List("1", "2", "3", "4", "5")
val invalidNumbers = List("1", "abc", "3")
println(s"Valid parsing: ${parseAllInts(validNumbers)}")
println(s"Invalid parsing: ${parseAllInts(invalidNumbers)}")
// Parallel processing with Parallel type class
import cats.Parallel
import cats.instances.either._
// Simulate async operations
def fetchUser(id: Int): Either[String, String] = {
if (id > 0) Right(s"User$id") else Left(s"Invalid user ID: $id")
}
def fetchPosts(userId: String): Either[String, List[String]] = {
Right(List(s"${userId}_post1", s"${userId}_post2"))
}
def fetchProfile(userId: String): Either[String, String] = {
Right(s"${userId}_profile")
}
// Sequential vs parallel execution
def fetchUserDataSequential(id: Int): Either[String, (String, List[String], String)] = {
for {
user <- fetchUser(id)
posts <- fetchPosts(user)
profile <- fetchProfile(user)
} yield (user, posts, profile)
}
// This would run in parallel if using async types like IO
def fetchUserDataParallel(id: Int): Either[String, (String, List[String], String)] = {
(fetchUser(id), fetchUser(id).flatMap(fetchPosts), fetchUser(id).flatMap(fetchProfile)).parMapN {
case (user, posts, profile) => (user, posts, profile)
}
}
println(s"Sequential fetch: ${fetchUserDataSequential(123)}")
println(s"Parallel fetch: ${fetchUserDataParallel(123)}")
Cats Effect - Functional Effects and Concurrency
import cats.effect._
import cats.effect.unsafe.implicits.global
import scala.concurrent.duration._
// Basic IO operations
val helloWorld: IO[Unit] = IO.println("Hello, World!")
val greeting: IO[String] = IO.pure("Hello")
val readLine: IO[String] = IO.readLine
// Running IO programs
helloWorld.unsafeRunSync()
val result = greeting.unsafeRunSync()
println(s"Greeting result: $result")
// Error handling in IO
def divide(a: Int, b: Int): IO[Double] = {
if (b == 0) IO.raiseError(new ArithmeticException("Division by zero"))
else IO.pure(a.toDouble / b)
}
val safeComputation = divide(10, 2).handleErrorWith { error =>
IO.println(s"Error occurred: ${error.getMessage}") *> IO.pure(0.0)
}
val errorComputation = divide(10, 0).handleErrorWith { error =>
IO.println(s"Error occurred: ${error.getMessage}") *> IO.pure(Double.NaN)
}
println(s"Safe result: ${safeComputation.unsafeRunSync()}")
println(s"Error result: ${errorComputation.unsafeRunSync()}")
// Resource management
def createFileResource(path: String): Resource[IO, java.io.PrintWriter] = {
Resource.make(
IO.delay(new java.io.PrintWriter(path)) // Acquire
)(writer =>
IO.delay(writer.close()) *> IO.println(s"Closed file: $path") // Release
)
}
val writeToFile: IO[Unit] = createFileResource("output.txt").use { writer =>
IO.delay {
writer.println("Hello from Cats Effect!")
writer.println("Resource management is automatic")
}
}
// This would create and automatically close the file
// writeToFile.unsafeRunSync()
// Concurrent operations
def simulateWork(name: String, duration: Duration): IO[String] = {
IO.sleep(duration) *> IO.pure(s"$name completed")
}
val concurrentWork: IO[List[String]] = List(
simulateWork("Task 1", 100.millis),
simulateWork("Task 2", 200.millis),
simulateWork("Task 3", 150.millis)
).parSequence
// Fiber-based concurrency
val fiberExample: IO[Unit] = for {
fiber1 <- simulateWork("Background task 1", 500.millis).start
fiber2 <- simulateWork("Background task 2", 300.millis).start
_ <- IO.println("Started background tasks")
result1 <- fiber1.join
result2 <- fiber2.join
_ <- IO.println(s"Results: ${result1.getOrElse("Failed")}, ${result2.getOrElse("Failed")}")
} yield ()
println("Running fiber example...")
fiberExample.unsafeRunSync()
// Producer-Consumer with concurrent queues
import cats.effect.std.Queue
def producer(queue: Queue[IO, String], items: List[String]): IO[Unit] = {
items.traverse_(item =>
IO.println(s"Producing: $item") *> queue.offer(item) *> IO.sleep(100.millis)
)
}
def consumer(queue: Queue[IO, String], name: String): IO[Unit] = {
def consumeLoop: IO[Unit] = for {
item <- queue.take
_ <- IO.println(s"$name consumed: $item")
_ <- IO.sleep(150.millis)
_ <- consumeLoop
} yield ()
consumeLoop
}
val producerConsumerExample: IO[Unit] = for {
queue <- Queue.bounded[IO, String](10)
items = List("item1", "item2", "item3", "item4", "item5")
// Start producer and consumer concurrently
producerFiber <- producer(queue, items).start
consumer1Fiber <- consumer(queue, "Consumer1").start
consumer2Fiber <- consumer(queue, "Consumer2").start
// Wait for producer to finish
_ <- producerFiber.join
// Give consumers time to process
_ <- IO.sleep(1.second)
// Cancel consumers
_ <- consumer1Fiber.cancel
_ <- consumer2Fiber.cancel
_ <- IO.println("Producer-consumer example completed")
} yield ()
// Uncomment to run:
// producerConsumerExample.unsafeRunSync()
// Ref for safe concurrent state
import cats.effect.Ref
def counterExample: IO[Unit] = for {
counter <- Ref.of[IO, Int](0)
// Multiple fibers incrementing counter
fibers <- (1 to 10).toList.traverse { i =>
(for {
_ <- IO.sleep((scala.util.Random.nextInt(100)).millis)
current <- counter.updateAndGet(_ + 1)
_ <- IO.println(s"Fiber $i: counter = $current")
} yield ()).start
}
// Wait for all fibers to complete
_ <- fibers.traverse_(_.join)
finalValue <- counter.get
_ <- IO.println(s"Final counter value: $finalValue")
} yield ()
println("Running counter example...")
counterExample.unsafeRunSync()
// Deferred for coordination
import cats.effect.Deferred
def coordinationExample: IO[Unit] = for {
signal <- Deferred[IO, String]
// Worker that waits for signal
worker <- (for {
_ <- IO.println("Worker waiting for signal...")
message <- signal.get
_ <- IO.println(s"Worker received: $message")
} yield ()).start
// Coordinator that sends signal after delay
coordinator <- (for {
_ <- IO.sleep(2.seconds)
_ <- IO.println("Coordinator sending signal...")
_ <- signal.complete("Start working!")
} yield ()).start
// Wait for both to complete
_ <- worker.join
_ <- coordinator.join
} yield ()
println("Running coordination example...")
coordinationExample.unsafeRunSync()
// Async boundary and thread pools
import cats.effect.std.Console
def cpuIntensiveTask(n: Int): IO[Long] = IO.blocking {
// Simulate CPU-intensive work
(1L to n).sum
}
def ioTask(message: String): IO[Unit] = IO.blocking {
// Simulate I/O work
Thread.sleep(100)
println(s"I/O Task: $message")
}
val mixedWorkload: IO[Unit] = for {
_ <- IO.println("Starting mixed workload...")
// CPU tasks on compute pool
cpuResults <- List(1000000, 2000000, 3000000).traverse(cpuIntensiveTask)
// I/O tasks on blocking pool
_ <- List("File 1", "File 2", "File 3").parTraverse(ioTask)
_ <- IO.println(s"CPU results: ${cpuResults.sum}")
_ <- IO.println("Mixed workload completed")
} yield ()
println("Running mixed workload example...")
mixedWorkload.unsafeRunSync()
// Stream processing with fs2 integration
import fs2.Stream
def streamExample: IO[Unit] = {
val numbers: Stream[IO, Int] = Stream.range(1, 11)
val processed: Stream[IO, String] = numbers
.evalMap(n => IO.delay(n * n)) // Square each number
.filter(_ % 2 == 0) // Keep only even squares
.map(n => s"Even square: $n") // Format as string
.evalTap(s => IO.println(s)) // Print each result
processed.compile.drain // Run the stream
}
println("Running stream example...")
streamExample.unsafeRunSync()
Web Development Frameworks
Akka HTTP - Reactive Web Services
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.stream.ActorMaterializer
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
// Basic Akka HTTP server
object SimpleAkkaHttpServer {
implicit val system: ActorSystem = ActorSystem("simple-server")
implicit val materializer: ActorMaterializer = ActorMaterializer()
implicit val executionContext: ExecutionContext = system.dispatcher
// Data models
case class User(id: Int, name: String, email: String)
case class CreateUserRequest(name: String, email: String)
case class ApiResponse[T](success: Boolean, data: Option[T] = None, message: Option[String] = None)
// In-memory user store (in real app, use database)
var users: Map[Int, User] = Map(
1 -> User(1, "Alice", "alice@example.com"),
2 -> User(2, "Bob", "bob@example.com")
)
var nextId = 3
// JSON support with spray-json
import spray.json._
import spray.json.DefaultJsonProtocol._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
implicit val userFormat: RootJsonFormat[User] = jsonFormat3(User)
implicit val createUserFormat: RootJsonFormat[CreateUserRequest] = jsonFormat2(CreateUserRequest)
implicit val apiResponseFormat: RootJsonFormat[ApiResponse[User]] = jsonFormat3(ApiResponse[User])
implicit val apiResponseUsersFormat: RootJsonFormat[ApiResponse[List[User]]] = jsonFormat3(ApiResponse[List[User]])
// Routes
def userRoutes: Route = {
pathPrefix("api" / "users") {
concat(
// GET /api/users - list all users
pathEnd {
get {
complete(ApiResponse(success = true, data = Some(users.values.toList)))
}
},
// GET /api/users/{id} - get specific user
path(IntNumber) { userId =>
get {
users.get(userId) match {
case Some(user) => complete(ApiResponse(success = true, data = Some(user)))
case None => complete(StatusCodes.NotFound, ApiResponse[User](success = false, message = Some("User not found")))
}
}
},
// POST /api/users - create new user
pathEnd {
post {
entity(as[CreateUserRequest]) { request =>
val newUser = User(nextId, request.name, request.email)
users += nextId -> newUser
nextId += 1
complete(StatusCodes.Created, ApiResponse(success = true, data = Some(newUser)))
}
}
},
// PUT /api/users/{id} - update user
path(IntNumber) { userId =>
put {
entity(as[CreateUserRequest]) { request =>
users.get(userId) match {
case Some(_) =>
val updatedUser = User(userId, request.name, request.email)
users += userId -> updatedUser
complete(ApiResponse(success = true, data = Some(updatedUser)))
case None =>
complete(StatusCodes.NotFound, ApiResponse[User](success = false, message = Some("User not found")))
}
}
}
},
// DELETE /api/users/{id} - delete user
path(IntNumber) { userId =>
delete {
users.get(userId) match {
case Some(user) =>
users -= userId
complete(ApiResponse(success = true, data = Some(user), message = Some("User deleted")))
case None =>
complete(StatusCodes.NotFound, ApiResponse[User](success = false, message = Some("User not found")))
}
}
}
)
}
}
// Health check route
def healthRoute: Route = {
path("health") {
get {
complete(Map("status" -> "ok", "timestamp" -> System.currentTimeMillis()))
}
}
}
// Static content route
def staticRoute: Route = {
pathPrefix("static") {
getFromResourceDirectory("static")
}
}
// Main routes
val routes: Route = concat(
userRoutes,
healthRoute,
staticRoute,
pathSingleSlash {
complete(HttpResponse(StatusCodes.OK, entity = "Welcome to Akka HTTP API"))
}
)
def startServer(host: String = "localhost", port: Int = 8080): Unit = {
val bindingFuture = Http().bindAndHandle(routes, host, port)
bindingFuture.onComplete {
case Success(binding) =>
println(s"Server running at http://$host:$port/")
println(s"Server bound to ${binding.localAddress}")
case Failure(exception) =>
println(s"Failed to bind to $host:$port: ${exception.getMessage}")
system.terminate()
}
}
}
// Advanced Akka HTTP features
object AdvancedAkkaHttpServer {
import akka.http.scaladsl.server.directives.Credentials
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import scala.concurrent.duration._
// Authentication
def authenticateBasic(credentials: Credentials): Option[String] = {
credentials match {
case p @ Credentials.Provided(username) if p.verify("secret") => Some(username)
case _ => None
}
}
// Middleware for request logging
import akka.http.scaladsl.server.directives.LoggingMagnet
import akka.event.Logging
def requestLogger = LoggingMagnet(_ => {
case r @ HttpRequest(method, uri, _, _, _) =>
println(s"${method.value} ${uri}")
r
})
// CORS support
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.model.HttpMethods._
def corsHandler: Directive0 = {
respondWithHeaders(
`Access-Control-Allow-Origin`.*,
`Access-Control-Allow-Methods`(GET, POST, PUT, DELETE, OPTIONS),
`Access-Control-Allow-Headers`("Authorization", "Content-Type")
)
}
// Rate limiting (simplified)
import scala.collection.mutable
val requestCounts: mutable.Map[String, (Long, Int)] = mutable.Map()
def rateLimit(maxRequests: Int, windowMs: Long): Directive0 = {
extractClientIP { ip =>
val clientIp = ip.toString
val now = System.currentTimeMillis()
requestCounts.get(clientIp) match {
case Some((windowStart, count)) if now - windowStart < windowMs =>
if (count >= maxRequests) {
complete(StatusCodes.TooManyRequests, "Rate limit exceeded")
} else {
requestCounts(clientIp) = (windowStart, count + 1)
pass
}
case _ =>
requestCounts(clientIp) = (now, 1)
pass
}
}
}
// Streaming response
def streamingRoute: Route = {
path("stream") {
get {
import akka.stream.scaladsl.Source
import akka.util.ByteString
val source = Source(1 to 100)
.map(i => s"data: Item $i\n")
.map(ByteString.apply)
.throttle(1, 100.millis)
complete(HttpResponse(
entity = HttpEntity.Chunked.fromData(ContentTypes.`text/plain(UTF-8)`, source)
))
}
}
}
// WebSocket support
import akka.http.scaladsl.model.ws.{Message, TextMessage}
import akka.stream.scaladsl.{Flow, Sink, Source}
def websocketRoute: Route = {
path("ws") {
get {
val greeterWebSocketService = Flow[Message]
.mapConcat {
case tm: TextMessage =>
TextMessage(Source.single(s"Echo: ${tm.textStream.runFold("")(_ + _)}")) :: Nil
case _ =>
Nil
}
handleWebSocketMessages(greeterWebSocketService)
}
}
}
// Complete advanced routes
val advancedRoutes: Route = {
logRequestResult(requestLogger) {
corsHandler {
rateLimit(100, 60000) { // 100 requests per minute
concat(
pathPrefix("api") {
authenticateBasic("realm", authenticateBasic) { username =>
pathPrefix("secure") {
complete(s"Hello, $username! This is a secure endpoint.")
}
}
},
streamingRoute,
websocketRoute
)
}
}
}
}
}
// Testing Akka HTTP routes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class UserRoutesSpec extends AnyWordSpec with Matchers with ScalatestRouteTest {
"User routes" should {
"return all users on GET /api/users" in {
Get("/api/users") ~> SimpleAkkaHttpServer.userRoutes ~> check {
status shouldEqual StatusCodes.OK
// Additional response checking would go here
}
}
"create a new user on POST /api/users" in {
val newUser = SimpleAkkaHttpServer.CreateUserRequest("Charlie", "charlie@example.com")
Post("/api/users", newUser) ~> SimpleAkkaHttpServer.userRoutes ~> check {
status shouldEqual StatusCodes.Created
// Check response body
}
}
"return 404 for non-existent user" in {
Get("/api/users/999") ~> SimpleAkkaHttpServer.userRoutes ~> check {
status shouldEqual StatusCodes.NotFound
}
}
}
}
// Usage example
object AkkaHttpApp extends App {
// This would start the server in a real application
// SimpleAkkaHttpServer.startServer("localhost", 8080)
// For demonstration, just show that the routes are defined
println("Akka HTTP server routes defined successfully")
println("To run: call SimpleAkkaHttpServer.startServer()")
}
Play Framework - Full-Stack Web Applications
// Play Framework application structure
// app/controllers/HomeController.scala
import javax.inject._
import play.api.mvc._
import play.api.libs.json._
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents)
(implicit ec: ExecutionContext) extends BaseController {
def index() = Action { implicit request: Request[AnyContent] =>
Ok(views.html.index())
}
def hello(name: String) = Action {
Ok(s"Hello, $name!")
}
def api() = Action {
Ok(Json.obj(
"message" -> "Welcome to the API",
"timestamp" -> System.currentTimeMillis(),
"version" -> "1.0.0"
))
}
}
// app/controllers/UserController.scala
import models.{User, UserRepository}
import javax.inject._
@Singleton
class UserController @Inject()(
val controllerComponents: ControllerComponents,
userRepository: UserRepository
)(implicit ec: ExecutionContext) extends BaseController {
implicit val userWrites: Writes[User] = Json.writes[User]
implicit val userReads: Reads[User] = Json.reads[User]
def list() = Action.async { implicit request =>
userRepository.findAll().map { users =>
Ok(Json.toJson(users))
}
}
def show(id: Long) = Action.async { implicit request =>
userRepository.findById(id).map {
case Some(user) => Ok(Json.toJson(user))
case None => NotFound(Json.obj("error" -> "User not found"))
}
}
def create() = Action.async(parse.json) { implicit request =>
request.body.validate[User].fold(
errors => Future.successful(BadRequest(Json.obj("errors" -> JsError.toJson(errors)))),
user => {
userRepository.create(user).map { createdUser =>
Created(Json.toJson(createdUser))
}
}
)
}
def update(id: Long) = Action.async(parse.json) { implicit request =>
request.body.validate[User].fold(
errors => Future.successful(BadRequest(Json.obj("errors" -> JsError.toJson(errors)))),
user => {
userRepository.update(id, user).map {
case Some(updatedUser) => Ok(Json.toJson(updatedUser))
case None => NotFound(Json.obj("error" -> "User not found"))
}
}
)
}
def delete(id: Long) = Action.async { implicit request =>
userRepository.delete(id).map { deleted =>
if (deleted) Ok(Json.obj("message" -> "User deleted"))
else NotFound(Json.obj("error" -> "User not found"))
}
}
}
// app/models/User.scala
case class User(
id: Option[Long] = None,
name: String,
email: String,
createdAt: Option[java.time.Instant] = None
)
// app/models/UserRepository.scala
import javax.inject.{Inject, Singleton}
import play.api.db.slick.DatabaseConfigProvider
import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class UserRepository @Inject()(dbConfigProvider: DatabaseConfigProvider)
(implicit ec: ExecutionContext) {
private val dbConfig = dbConfigProvider.get[JdbcProfile]
import dbConfig._
import profile.api._
class UserTable(tag: Tag) extends Table[User](tag, "users") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def email = column[String]("email")
def createdAt = column[java.time.Instant]("created_at")
def * = (id.?, name, email, createdAt.?) <> ((User.apply _).tupled, User.unapply)
}
private val users = TableQuery[UserTable]
def findAll(): Future[Seq[User]] = db.run(users.result)
def findById(id: Long): Future[Option[User]] =
db.run(users.filter(_.id === id).result.headOption)
def create(user: User): Future[User] = {
val userWithTimestamp = user.copy(createdAt = Some(java.time.Instant.now()))
db.run(
(users returning users.map(_.id) into ((user, id) => user.copy(id = Some(id))))
+= userWithTimestamp
)
}
def update(id: Long, user: User): Future[Option[User]] = {
val updateAction = users.filter(_.id === id).update(user.copy(id = Some(id)))
db.run(updateAction).flatMap { rowsUpdated =>
if (rowsUpdated > 0) findById(id)
else Future.successful(None)
}
}
def delete(id: Long): Future[Boolean] = {
db.run(users.filter(_.id === id).delete).map(_ > 0)
}
}
// conf/routes
"""
# Routes
# This file defines all application routes (Higher priority routes first)
# Home page
GET / controllers.HomeController.index
GET /hello/:name controllers.HomeController.hello(name: String)
# API endpoints
GET /api controllers.HomeController.api
# User API
GET /api/users controllers.UserController.list
GET /api/users/:id controllers.UserController.show(id: Long)
POST /api/users controllers.UserController.create
PUT /api/users/:id controllers.UserController.update(id: Long)
DELETE /api/users/:id controllers.UserController.delete(id: Long)
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
"""
// conf/application.conf
"""
# Database configuration
slick.dbs.default.profile="slick.jdbc.H2Profile$"
slick.dbs.default.db.driver="org.h2.Driver"
slick.dbs.default.db.url="jdbc:h2:mem:play;MODE=MYSQL"
# Play configuration
play.http.secret.key = "changeme"
play.i18n.langs = [ "en" ]
# Application configuration
app.name = "Play Scala Application"
app.version = "1.0.0"
"""
// app/views/index.scala.html (Twirl template)
"""
@()
@main("Welcome to Play") {
<h1>Welcome to Play!</h1>
<p>This is a Scala Play Framework application.</p>
<h2>API Endpoints</h2>
<ul>
<li><a href="/api">API Info</a></li>
<li><a href="/api/users">List Users</a></li>
<li><a href="/hello/World">Hello World</a></li>
</ul>
}
"""
// app/views/main.scala.html
"""
@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
@* Here's where we render the page title `String`. *@
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
</head>
<body>
@* And here's where we render the `Html` object containing
* the page content. *@
@content
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
</body>
</html>
"""
// Module for dependency injection (app/modules/AppModule.scala)
import com.google.inject.AbstractModule
import play.api.{Configuration, Environment}
class AppModule(environment: Environment, configuration: Configuration) extends AbstractModule {
override def configure(): Unit = {
// Bind services and repositories
bind(classOf[UserRepository]).asEagerSingleton()
// Custom configuration bindings
bind(classOf[String]).annotatedWith(Names.named("app.name"))
.toInstance(configuration.get[String]("app.name"))
}
}
// Action composition for authentication (app/actions/AuthAction.scala)
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}
case class UserRequest[A](userId: Long, request: Request[A]) extends WrappedRequest[A](request)
class AuthAction @Inject()(val parser: BodyParsers.Default)
(implicit val executionContext: ExecutionContext)
extends ActionBuilder[UserRequest, AnyContent] with ActionTransformer[Request, UserRequest] {
def transform[A](request: Request[A]) = Future.successful {
// In real application, extract user from session/token
val userId = request.session.get("userId").map(_.toLong).getOrElse(1L)
UserRequest(userId, request)
}
}
// Usage in controller with authentication
class SecureController @Inject()(
val controllerComponents: ControllerComponents,
authAction: AuthAction
) extends BaseController {
def profile() = authAction { implicit request: UserRequest[AnyContent] =>
Ok(Json.obj(
"userId" -> request.userId,
"message" -> "This is your profile"
))
}
}
// Testing Play applications
import org.scalatest._
import org.scalatestplus.play._
import play.api.test._
import play.api.test.Helpers._
class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
"HomeController GET" should {
"render the index page from a new instance of controller" in {
val controller = new HomeController(stubControllerComponents())
val home = controller.index().apply(FakeRequest(GET, "/"))
status(home) mustBe OK
contentType(home) mustBe Some("text/html")
contentAsString(home) must include ("Welcome to Play")
}
"render the index page from the application" in {
val controller = inject[HomeController]
val home = controller.index().apply(FakeRequest(GET, "/"))
status(home) mustBe OK
contentType(home) mustBe Some("text/html")
contentAsString(home) must include ("Welcome to Play")
}
"render the index page from the router" in {
val request = FakeRequest(GET, "/")
val home = route(app, request).get
status(home) mustBe OK
contentType(home) mustBe Some("text/html")
contentAsString(home) must include ("Welcome to Play")
}
}
}
println("Play Framework examples defined successfully")
Summary
In this lesson, you've explored the rich Scala ecosystem:
✅ Build Tools: SBT configuration, multi-module projects, and plugin integration
✅ Configuration: Type-safe configuration management with Typesafe Config
✅ Functional Libraries: Cats for functional programming abstractions
✅ Effect Systems: Cats Effect for safe concurrency and resource management
✅ Web Frameworks: Akka HTTP for reactive services and Play for full-stack apps
✅ Testing: Integration testing and testing frameworks
✅ Best Practices: Dependency injection, error handling, and production patterns
Understanding the ecosystem empowers you to choose the right tools and build production-ready Scala applications efficiently.
What's Next
In the next lesson, we'll explore advanced features and patterns including performance optimization, debugging techniques, and preparing Scala applications for production deployment.
Comments
Be the first to comment on this lesson!