scala Tutorial
Building an Expense Tracker with Scala 3 - Part 1: Setup
In this series
- •Building an Expense Tracker with Scala 3 - Part 1: Setup
- •Building an Expense Tracker with Scala 3 - Part 2: Database & Docker
- •Building an Expense Tracker with Scala 3 - Part 3: Authentication
- •Building an Expense Tracker with Scala 3 - Part 4: Expenses & Categories
- •Building an Expense Tracker with Scala 3 - Part 5: Internationalization & Testing
Welcome to this comprehensive tutorial series where we will build a production-ready Expense Tracker API using Scala 3, Play Framework, Macwire, and Pekko.
In this first part, we will set up our development environment, initialize the project, and create a simple "Hello World" endpoint. We'll focus on getting the foundation right, using modern Scala 3 syntax and compile-time dependency injection.
Prerequisites
Before we begin, ensure you have the following installed:
- Java JDK 11+ (We recommend JDK 17 or 21 for better performance and features)
- sbt (Scala Build Tool) - The standard build tool for Scala projects.
- Docker (for later parts) - We'll use this to run our database.
Project Initialization
We'll start by creating a new directory for our project and initializing the build.sbt file. This file defines your project's settings and dependencies.
// build.sbt
name := """expense-tracker-api"""
organization := "com.calculusdev"
version := "1.0-SNAPSHOT"
// We enable the PlayScala plugin to get all the Play Framework goodies
lazy val root = (project in file(".")).enablePlugins(PlayScala)
scalaVersion := "3.3.3"
libraryDependencies ++= Seq(
guice, // Required for Play's internal workings, even if we use Macwire
"org.scalatestplus.play" %% "scalatestplus-play" % "7.0.1" % Test,
"com.softwaremill.macwire" %% "macros" % "2.5.9" % "provided", // For compile-time DI
"mysql" % "mysql-connector-j" % "8.3.0", // JDBC driver for MySQL
"org.apache.pekko" %% "pekko-actor-typed" % "1.0.2",
"org.apache.pekko" %% "pekko-stream" % "1.0.2"
)
Why these dependencies?
- Macwire: Play Framework uses Guice (Runtime DI) by default. We're switching to Macwire for Compile-time Dependency Injection. This means if you forget to wire a component, your code won't compile, rather than crashing at runtime. It's safer and cleaner.
- Pekko: Since Akka changed its license, Pekko is the open-source fork we use for reactive streams and actor systems.
- MySQL Connector: We'll need this in Part 2 to talk to our database.
Dependency Injection with Macwire
Instead of using Guice's magic @Inject annotations that are resolved at runtime, we will explicitly wire our application. This gives us full control over how our components are created.
Create an AppLoader.scala file in the app directory. This class tells Play how to load our application.
// app/AppLoader.scala
import play.api.ApplicationLoader.Context
import play.api.*
import play.api.mvc.EssentialFilter
import play.api.routing.Router
import play.filters.HttpFiltersComponents
import controllers.HomeController
import router.Routes
import com.softwaremill.macwire.*
class AppLoader extends ApplicationLoader {
def load(context: Context): Application = {
// We instantiate our components and return the application
new AppComponents(context).application
}
}
class AppComponents(context: Context) extends BuiltInComponentsFromContext(context)
with HttpFiltersComponents
with AssetsComponents {
// wire[] is a macro that finds a class constructor and fills in the dependencies
// found in the current scope (like this class).
lazy val homeController: HomeController = wire[HomeController]
lazy val router: Router = {
val prefix: String = "/"
wire[Routes]
}
}
Creating the Controller
Now, let's create a simple HomeController to verify everything is working. Controllers handle incoming HTTP requests and return responses.
// app/controllers/HomeController.scala
package controllers
import play.api.mvc.*
// Notice we don't use @Inject here. We just define the constructor arguments.
class HomeController(cc: ControllerComponents) extends AbstractController(cc) {
def index = Action {
Ok("Expense Tracker API is running!")
}
}
Configuration
Finally, we need to tell Play to use our custom AppLoader instead of the default Guice loader. Add this to conf/application.conf:
play.application.loader = "AppLoader"
play.i18n.langs = ["en", "es"]
Running the Application
To run the application, execute the following command in your terminal:
sbt run
Open your browser and navigate to http://localhost:9000. You should see the message: "Expense Tracker API is running!"
Next Steps
In the next part, we will set up MySQL with Docker Compose and connect our application to the database. We'll also create a DatabaseService to manage our connections.
Continue to Part 2: Database & Docker
