A complete REST API built with Scala, Play Framework, and Slick ORM demonstrating modern functional programming concepts and professional backend development patterns.
This project covers essential Scala and backend development concepts:
- Scala Fundamentals: Case classes, sealed traits, pattern matching, Option/Either types
- Functional Programming: Immutable data structures, pure functions, type safety
- Play Framework: REST API development, dependency injection, routing
- Slick ORM: Type-safe database queries, functional database access
- Database Design: Schema management, evolutions, relationships
- API Design: RESTful endpoints, JSON serialization, error handling
- Testing: Comprehensive API testing strategies
- Project Overview
- Setup Instructions
- Project Structure
- Core Concepts
- API Documentation
- Code Flow Explanation
- Scala Concepts Used
- Testing Guide
- Troubleshooting
- Learning Resources
The Task Management API allows you to:
- Create, read, update, and delete tasks
- Set task priorities (Low, Medium, High)
- Mark tasks as completed
- Set due dates for tasks
- Filter tasks by status, priority, and due dates
- Get task statistics and analytics
- Language: Scala 2.13
- Framework: Play Framework 2.9
- Database: MySQL 8.0 (via Docker)
- ORM: Slick 3.4
- Build Tool: SBT (Scala Build Tool)
- JSON: Play JSON library
- Dependency Injection: Google Guice
- Java 11+: Download from Oracle or use OpenJDK
- SBT: Install from scala-sbt.org
- Docker: Install from docker.com
- VS Code (recommended): With Scala (Metals) extension
-
Clone/Create the Project
mkdir task-management-api cd task-management-api -
Set Up Database (MySQL via Docker)
docker run --name task-mysql \ -e MYSQL_ROOT_PASSWORD=rootpass \ -e MYSQL_DATABASE=taskapi \ -e MYSQL_USER=taskuser \ -e MYSQL_PASSWORD=taskpass \ -p 3306:3306 \ -d mysql:8.0
-
Create Project Files
Copy all the project files shown in the Project Structure section below.
-
Run the Application
sbt clean sbt compile sbt run
-
Test the API
Visit:
http://localhost:9000/api/tasks
task-management-api/
βββ build.sbt # Build configuration and dependencies
βββ project/
β βββ build.properties # SBT version
β βββ plugins.sbt # SBT plugins (Play Framework)
βββ conf/
β βββ application.conf # Application configuration
β βββ routes # URL routing definitions
β βββ evolutions/
β βββ default/
β βββ 1.sql # Database schema migration
βββ app/
β βββ controllers/
β β βββ HomeController.scala # Basic endpoints (health, home)
β β βββ TaskController.scala # Task API endpoints
β βββ models/
β β βββ Task.scala # Data models and DTOs
β βββ services/
β β βββ TaskService.scala # Business logic layer
β βββ repositories/
β β βββ TaskRepository.scala # Database access layer
β βββ Module.scala # Dependency injection configuration
βββ test/
βββ (test files)
build.sbt: Defines project dependencies, Scala version, and build settingsconf/application.conf: Database connections, Play Framework configurationconf/routes: Maps HTTP requests to controller methodsmodels/Task.scala: Data structures representing your domaincontrollers/TaskController.scala: Handles HTTP requests and responsesservices/TaskService.scala: Contains business logic and validationrepositories/TaskRepository.scala: Database operations using Slick ORM
HTTP Request β Controller β Service β Repository β Database
β
HTTP Response β Controller β Service β Repository β Database
- Controller: Handles HTTP requests/responses, JSON serialization
- Service: Business logic, validation, orchestration
- Repository: Database operations, query building
- Model: Data structures and domain objects
@Singleton
class TaskController @Inject()(
val controllerComponents: ControllerComponents,
taskService: TaskService // β Injected dependency
)(implicit ec: ExecutionContext) extends BaseControllerDependencies are automatically provided by the DI framework.
- Immutability: All data structures are immutable by default
- Pure Functions: Functions without side effects
- Type Safety: Compile-time error checking
- Pattern Matching: Safe conditional logic
http://localhost:9000
None required for this demo API.
All POST/PUT requests expect Content-Type: application/json
Get all tasks
Response:
{
"status": "success",
"data": [
{
"id": 1,
"title": "Learn Scala fundamentals",
"description": "Study case classes, pattern matching",
"completed": false,
"priority": 3,
"dueDate": "2025-06-24T10:00:00",
"createdAt": "2025-06-17T18:30:00",
"updatedAt": "2025-06-17T18:30:00"
}
],
"count": 1
}Get specific task by ID
Example: GET /api/tasks/1
Create a new task
Request Body:
{
"title": "New Task",
"description": "Task description",
"priority": 2,
"completed": false,
"dueDate": "2025-06-25T10:00:00"
}Response:
{
"status": "success",
"data": {
"id": 5,
"title": "New Task",
"description": "Task description",
"completed": false,
"priority": 2,
"dueDate": "2025-06-25T10:00:00",
"createdAt": "2025-06-17T18:35:00",
"updatedAt": "2025-06-17T18:35:00"
}
}Update an existing task
Request Body: Same as POST
Delete a task
Response:
{
"status": "success",
"message": "Task deleted"
}Mark task as completed
Get tasks by priority level
1= Low priority2= Medium priority3= High priority
Example: GET /api/tasks/priority/3
Get all completed tasks
Get all pending (incomplete) tasks
Get all overdue tasks
Get task statistics
Response:
{
"status": "success",
"data": {
"total": 10,
"completed": 3,
"pending": 7,
"high_priority": 2,
"medium_priority": 5,
"low_priority": 3,
"overdue": 1
}
}API health check
Welcome message
Let's trace through what happens when you make a request:
POST /api/tasks controllers.TaskController.createTaskPlay Framework maps the HTTP request to the controller method.
def createTask() = Action.async(parse.json) { implicit request: Request[JsValue] =>
request.body.validate[TaskInput] match {
case JsSuccess(taskInput, _) =>
taskService.createFromInput(taskInput).map {
case Right(createdTask) =>
Created(Json.obj("status" -> "success", "data" -> createdTask))
case Left(error) =>
BadRequest(Json.obj("status" -> "error", "message" -> error))
}
// ... error handling
}
}What happens:
- Parses JSON request body
- Validates JSON structure against
TaskInputmodel - Calls service layer for business logic
- Returns appropriate HTTP response
def createFromInput(taskInput: TaskInput): Future[Either[String, Task]] = {
TaskInput.validate(taskInput) match {
case Left(error) => Future.successful(Left(error))
case Right(validTaskInput) =>
val task = TaskInput.toTask(validTaskInput)
taskRepository.create(task).map(Right(_))
}
}What happens:
- Validates business rules (title not empty, etc.)
- Converts DTO to domain model
- Calls repository for database operations
- Returns
Either[Error, Success]for functional error handling
def create(task: Task): Future[Task] = {
val now = LocalDateTime.now()
val taskWithTimestamps = task.copy(createdAt = now, updatedAt = now)
val insertQuery = tasks returning tasks.map(_.id) into ((task, id) => task.copy(id = id))
db.run(insertQuery += taskWithTimestamps)
}What happens:
- Sets timestamps automatically
- Builds type-safe SQL query using Slick
- Executes query and returns the created task with generated ID
INSERT INTO tasks (title, description, completed, priority, due_date, created_at, updated_at)
VALUES ('New Task', 'Description', FALSE, 2, '2025-06-25 10:00:00', NOW(), NOW());The database stores the task and returns the generated ID.
case class Task(
id: Option[Long] = None,
title: String,
description: Option[String] = None,
completed: Boolean = false,
priority: Priority = Priority.Low,
dueDate: Option[LocalDateTime] = None,
createdAt: LocalDateTime = LocalDateTime.now(),
updatedAt: LocalDateTime = LocalDateTime.now()
)Benefits:
- Automatic
equals,hashCode,toString - Immutable by default
- Pattern matching support
copymethod for updates
sealed trait Priority {
def value: Int
def name: String
}
object Priority {
case object Low extends Priority {
val value = 1
val name = "Low"
}
case object Medium extends Priority {
val value = 2
val name = "Medium"
}
case object High extends Priority {
val value = 3
val name = "High"
}
}Benefits:
- Compiler ensures exhaustive pattern matching
- Type-safe alternatives to magic numbers
- Cannot be extended outside the file
// Instead of nullable fields, use Option
description: Option[String] = None
// Safe usage
task.description match {
case Some(desc) => println(s"Description: $desc")
case None => println("No description")
}
// Or with functional methods
task.description.map(_.toUpperCase).getOrElse("NO DESCRIPTION")def validate(task: Task): Either[String, Task] = {
if (task.title.trim.isEmpty) {
Left("Task title cannot be empty") // Error case
} else {
Right(task) // Success case
}
}
// Usage
validate(task) match {
case Left(error) => handleError(error)
case Right(validTask) => processTask(validTask)
}def findAll(): Future[Seq[Task]] = {
db.run(tasks.result) // Returns Future[Seq[Task]]
}
// Composing Futures
for {
user <- userRepository.findById(1)
tasks <- taskRepository.findByUserId(1)
} yield (user, tasks)priority match {
case Priority.High => "Urgent!"
case Priority.Medium => "Important"
case Priority.Low => "When possible"
}
// With extraction
task match {
case Task(_, title, _, true, _, _, _, _) => s"Completed: $title"
case Task(_, title, _, false, Priority.High, _, _, _) => s"Urgent: $title"
case _ => "Regular task"
}// JSON serialization
implicit val taskFormat: Format[Task] = Json.format[Task]
// Custom column types
implicit val priorityMapper: BaseColumnType[Priority] = MappedColumnType.base[Priority, Int](
priority => priority.value,
value => Priority.fromInt(value).getOrElse(Priority.Low)
)// Functional operations on collections
val highPriorityTasks = tasks.filter(_.priority == Priority.High)
val taskTitles = tasks.map(_.title)
val completedCount = tasks.count(_.completed)
// Function composition
def filterAndSort(tasks: List[Task]): List[Task] = {
tasks
.filter(!_.completed)
.sortBy(_.priority.value)
.reverse
}Create a task:
curl -X POST http://localhost:9000/api/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "Learn Scala",
"description": "Study functional programming concepts",
"priority": 3,
"completed": false
}'Get all tasks:
curl http://localhost:9000/api/tasksUpdate a task:
curl -X PUT http://localhost:9000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{
"title": "Master Scala",
"description": "Deep dive into advanced concepts",
"priority": 3,
"completed": true
}'For GET requests, simply visit:
http://localhost:9000/api/taskshttp://localhost:9000/api/tasks/statshttp://localhost:9000/api/tasks/priority/3
-
Import the following endpoints:
- GET
http://localhost:9000/api/tasks - POST
http://localhost:9000/api/tasks - PUT
http://localhost:9000/api/tasks/1 - DELETE
http://localhost:9000/api/tasks/1
- GET
-
Set headers:
Content-Type: application/json -
Use request body for POST/PUT:
{ "title": "Test Task", "priority": 2 }
Save this as test-api.sh:
#!/bin/bash
BASE_URL="http://localhost:9000"
echo "π§ͺ Testing Task Management API"
# Test 1: Health check
echo "Testing health endpoint..."
curl -s "$BASE_URL/health" | jq '.'
# Test 2: Get all tasks
echo "Getting all tasks..."
curl -s "$BASE_URL/api/tasks" | jq '.'
# Test 3: Create a task
echo "Creating a new task..."
curl -s -X POST "$BASE_URL/api/tasks" \
-H "Content-Type: application/json" \
-d '{
"title": "Test Task",
"description": "Created by test script",
"priority": 2
}' | jq '.'
# Test 4: Get statistics
echo "Getting task statistics..."
curl -s "$BASE_URL/api/tasks/stats" | jq '.'
# Test 5: Filter by priority
echo "Getting high priority tasks..."
curl -s "$BASE_URL/api/tasks/priority/3" | jq '.'
echo "β
Tests completed!"Run with: chmod +x test-api.sh && ./test-api.sh
Error: object is not a member of package
[error] object Priority is not a member of package models
Solution: Check import statements:
import models.{Task, Priority}Error: Database connection failed
Solutions:
# Check Docker container
docker ps | grep mysql
# Restart MySQL container
docker restart task-mysql
# Check MySQL logs
docker logs task-mysqlError: Address already in use: bind
Solutions:
# Find process using port 9000
lsof -i :9000
# Kill the process
kill -9 <PID>
# Or use different port
sbt "run 9001"Error: Database needs evolution
Solutions:
- Visit
http://localhost:9000and click "Apply this script now!" - Or set
play.evolutions.db.default.autoApply = true
Error: Invalid JSON format
Solution: Ensure proper Content-Type header:
curl -H "Content-Type: application/json" -d '{"title":"Task"}'Add these to conf/application.conf for debugging:
# Enable SQL logging
db.default.logSql = true
# Enable detailed error messages
play.debug = true- Official Scala Documentation: docs.scala-lang.org
- Scala Exercises: scala-exercises.org
- "Programming in Scala" Book: The definitive Scala guide
- Play Framework Documentation: playframework.com
- Play Framework Tutorials: Step-by-step guides
- Slick Documentation: scala-slick.org
- Slick Examples: Database patterns and best practices
- "Functional Programming in Scala": Red book for FP concepts
- Cats Library: Advanced functional programming patterns
- RESTful API Guidelines: Best practices for API design
- HTTP Status Codes: Understanding proper status codes
- Add User Management: Create user entities and authentication
- Add Categories: Organize tasks into categories
- Add Search: Full-text search functionality
- Add Pagination: Handle large datasets efficiently
- Add Tests: Unit tests with ScalaTest
- Add Validation: Advanced validation with custom validators
- Add Caching: Redis integration for performance
- Add API Documentation: Swagger/OpenAPI integration
- Add Authentication: JWT tokens and user sessions
- Add Authorization: Role-based access control
- Add File Uploads: Task attachments functionality
- Add Notifications: Email/SMS notifications for due dates
- Add Monitoring: Metrics and logging with Akka monitoring
- Add Deployment: Docker containerization and cloud deployment
This is an educational project. Feel free to:
- Fork the repository
- Add new features
- Improve documentation
- Add tests
- Share your learnings
This project is for educational purposes. Feel free to use it as a learning resource.
If you encounter issues:
- Check the Troubleshooting section
- Review the error messages carefully
- Consult the official documentation for Play Framework and Slick
- Practice with simpler examples first
Remember: Learning Scala and functional programming takes time. Start with small steps and gradually build complexity!
Happy Coding! π
This README covers everything you need to understand and extend this Scala Play Framework API. Use it as a reference as you continue learning Scala and backend development!