This document provides a comprehensive analysis of the DecodingUs project, a Scala-based web application. It is intended to guide developers in understanding the architecture, extending its features, and improving test coverage.
DecodingUs is a modern web application built with Scala 3 and the Play Framework. It serves as a platform for genomic data analysis, with features related to haplogroups, biosamples, and scientific publications. The application exposes a REST API, serves web pages, and runs background jobs for data processing.
The project leverages a modern technology stack:
- Backend Framework: Play Framework
- Language: Scala 3
- Database: PostgreSQL
- Database Access: Slick
- API Definition: Tapir for OpenAPI/Swagger documentation
- Asynchronous Jobs: Apache Pekko with
pekko-quartz-scheduler - Dependency Injection: Guice
- Testing: ScalaTest with
scalatestplus-play - Build Tool: sbt
- Frontend Interactivity: HTMX for HATEOAS on HTML pages
The project follows a standard Play application layout with some key directories:
app/: Contains the core application source code.app/api/: Tapir endpoint definitions. These define the API structure for OpenAPI generation but do not contain business logic.app/controllers/: Play controllers that handle HTTP requests. Some controllers implement the logic for the API endpoints defined inapp/api/.app/services/: The business logic layer. Controllers delegate to services to perform operations.app/repositories/: The data access layer, responsible for all database interactions using Slick.app/models/: Domain models, API request/response objects, and Slick table definitions.app/modules/: Guice modules for dependency injection and application startup lifecycle.app/actors/: Pekko actors for concurrent and background processing tasks.
conf/: Configuration files.application.conf: The main configuration file for the application, including database connections, module loading, and scheduler settings.routes: The main routing file that maps HTTP requests to controller actions.
test/: Contains automated tests.build.sbt: The sbt build definition file, where all project dependencies are managed.
The project uses a hybrid approach for its API:
- Declarative Endpoints with Tapir: The
app/api/directory contains endpoint definitions using Tapir. These definitions describe the API's shape (URL, methods, inputs, outputs) and are used to generate a unified OpenAPI specification. - Implementation in Play Controllers: The actual logic for handling API requests is implemented in standard Play controllers located in
app/controllers/. - Routing: The
conf/routesfile maps API paths (e.g.,/api/...) tocontrollers.ApiRouter, which serves the Swagger UI. The same routes file also directs requests to the appropriate Play controllers that contain the business logic.
This separation allows for clear API documentation while leveraging the familiar Play Framework controller pattern for implementation.
The project utilizes HTMX to enhance the interactivity of HTML pages by enabling HATEOAS (Hypermedia as the Engine of Application State) principles. This approach allows for dynamic updates to parts of the page without full page reloads, using HTML attributes to trigger AJAX requests and swap content. This minimizes the need for extensive client-side JavaScript frameworks for common interactive patterns.
The application's startup and lifecycle are managed by Guice modules in the app/modules/ directory.
StartupModule.scala: This module eagerly binds aStartupService.StartupService: This service is responsible for initializing the application on startup. A key task it performs is seeding the database with essential data, such as importing haplogroup trees via theTreeInitializationService.Scheduler.scala: This module configures and schedules background jobs using the Pekko Quartz Scheduler. Job schedules are defined inconf/application.conf.
Tests are located in the test/ directory and are written using ScalaTest.
To run the existing test suite, execute the following command in your sbt shell:
sbt testWhen adding new features, it is crucial to add corresponding tests.
- Controller Tests: For new controllers, add a new spec file in
test/controllers/. Usescalatestplus-playto help create a test application instance and make requests to your controller actions. - Service Tests: For new services, create a new spec file in a corresponding package under
test/. Mock any repository dependencies to isolate the business logic for unit testing. - Repository Tests: For repository tests, you may need an in-memory or test database to verify database queries.
Here is a step-by-step guide to adding a new feature (e.g., a new API endpoint):
- Define the Endpoint (Tapir): If it's a new API endpoint, first define its structure in a new file within
app/api/. This makes it part of the OpenAPI specification. - Add Route: Add a new entry in the
conf/routesfile to route the new URL to a new controller action. - Create Controller: Create a new controller in
app/controllers/or add a new action to an existing one. This controller will handle the HTTP request. - Implement Service Logic: Create a new service in
app/services/to contain the business logic. The controller should call this service. - Implement Repository Logic: If the feature requires database access, create a new repository in
app/repositories/or add a new query method to an existing one. The service will use this repository. - Add Models: Define any new data structures (case classes) needed for the API request/response or for the database schema in the
app/models/directory. - Write Tests: Add new tests for the controller, service, and repository to ensure the feature works correctly and is protected against future regressions.