scala Tutorial
Building an Expense Tracker with Scala 3 - Part 4: Expenses & Categories
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
In this part, we will implement the core functionality of our application: managing Categories and Expenses. This involves creating CRUD (Create, Read, Update, Delete) operations.
We'll see how to model relationships between data (Expenses belong to Categories and Users) and how to expose these relationships via our API.
Models
We'll start by defining the data models. Notice how we use Option[Long] for IDs that are generated by the database.
// app/models/Category.scala
package models
case class Category(
id: Option[Long],
userId: Long, // Foreign key to User
name: String,
icon: String, // We'll store the icon name (e.g., "coffee") to render on the frontend
color: String // Hex color code
)
// app/models/Expense.scala
package models
import java.util.Date
case class Expense(
id: Option[Long],
userId: Long,
categoryId: Long, // Foreign key to Category
amount: BigDecimal, // Always use BigDecimal for money! Double can have precision errors.
currency: String,
description: String,
date: Date,
createdAt: Date
)
Repositories
Next, we implement the repositories to handle database operations. We'll continue using our DatabaseService helper.
Category Repository
// app/repositories/CategoryRepository.scala
package repositories
import models.Category
import services.DatabaseService
import java.sql.{ResultSet, Statement}
class CategoryRepository(db: DatabaseService) {
// Fetch all categories for a specific user
def list(userId: Long): Seq[Category] = {
db.withConnection { conn =>
val stmt = conn.prepareStatement("SELECT * FROM categories WHERE user_id = ?")
stmt.setLong(1, userId)
val rs = stmt.executeQuery()
// We iterate over the ResultSet and map each row to a Category object
val categories = scala.collection.mutable.ListBuffer[Category]()
while (rs.next()) {
categories += mapRow(rs)
}
categories.toSeq
}
}
def create(category: Category): Long = {
// ... insert logic similar to User and Expense
}
private def mapRow(rs: ResultSet): Category = {
Category(
id = Some(rs.getLong("id")),
userId = rs.getLong("user_id"),
name = rs.getString("name"),
icon = rs.getString("icon"),
color = rs.getString("color")
)
}
}
Expense Repository
// app/repositories/ExpenseRepository.scala
package repositories
import models.Expense
import services.DatabaseService
// ... imports
class ExpenseRepository(db: DatabaseService) {
// ... list and create methods similar to CategoryRepository
// Remember to handle the BigDecimal conversion correctly with rs.getBigDecimal()
}
Controllers
Now we create the controllers to expose these operations via the API. We'll use Play's Json.format macro to automatically generate JSON serializers/deserializers for our case classes.
// app/controllers/ExpenseController.scala
package controllers
import models.Expense
import play.api.mvc.*
import play.api.libs.json.*
import repositories.ExpenseRepository
import javax.inject.Inject
import scala.concurrent.ExecutionContext
class ExpenseController(cc: ControllerComponents, expenseRepo: ExpenseRepository)(implicit ec: ExecutionContext) extends AbstractController(cc) {
// This one line generates the JSON reader and writer for the Expense class!
implicit val expenseFormat: Format[Expense] = Json.format[Expense]
def list(userId: Long) = Action {
val expenses = expenseRepo.list(userId)
Ok(Json.toJson(expenses)) // Automatically converts Seq[Expense] to JSON array
}
def create(userId: Long) = Action(parse.json) { request =>
// ... validation and creation logic
// In a real app, verify that the `userId` in the path matches the authenticated user!
}
}
Wiring and Routes
Finally, wire everything up in AppLoader.scala and add the routes.
// app/AppLoader.scala
lazy val categoryController: CategoryController = wire[CategoryController]
lazy val expenseController: ExpenseController = wire[ExpenseController]
lazy val categoryRepository: repositories.CategoryRepository = wire[repositories.CategoryRepository]
lazy val expenseRepository: repositories.ExpenseRepository = wire[repositories.ExpenseRepository]
# conf/routes
# We use :userId in the route to identify whose resources we are accessing
GET /api/users/:userId/categories controllers.CategoryController.list(userId: Long)
POST /api/users/:userId/categories controllers.CategoryController.create(userId: Long)
GET /api/users/:userId/expenses controllers.ExpenseController.list(userId: Long)
POST /api/users/:userId/expenses controllers.ExpenseController.create(userId: Long)
Conclusion
We now have a fully functional API for tracking expenses! We can create users, log in, manage categories, and track expenses.
In the next and final part of the backend series, we will add Internationalization and write some Tests to ensure our code is robust.
Continue to Part 5: Internationalization & Testing
