API Architecture & Structure
Structuring a large Actix Web project.
Learning Objectives
- Understand the Controller-Service-Repository pattern
##Structuring an API that can grow
A single main.rs is fine for a demo, but a real service needs structure. Good architecture keeps code easy to change, test and understand as features pile up.
#Separate responsibilities
A clean API usually has these layers:
- Handlers (HTTP layer): parse requests, call the service, build responses. No business rules here.
- Services (domain layer): the business logic, independent of HTTP.
- Repositories (data layer): talk to the database. Services never write SQL directly.
- Models and DTOs: the shapes of data inside the system and over the wire.
A request flows downward and the result flows back up: handler, service, repository, database.
#A typical folder layout
src/
main.rs <- builds config, state, server
config.rs <- environment configuration
routes/
mod.rs <- registers all route modules
users.rs <- user handlers
services/
user_service.rs
repositories/
user_repo.rs
models/
user.rs
errors.rs <- one ApiError type#Keep main.rs tiny
main.rs should only load configuration, build shared state and start the server:
mod config;
mod errors;
mod routes;
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let cfg = config::Config::from_env();
let state = web::Data::new(AppState::new(&cfg).await);
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.configure(routes::configure)
})
.bind((cfg.host.as_str(), cfg.port))?
.run()
.await
}#A thin handler
pub async fn create_user(
state: web::Data<AppState>,
body: web::Json<CreateUser>,
) -> Result<HttpResponse, ApiError> {
let user = state.users.create(body.into_inner()).await?;
Ok(HttpResponse::Created().json(user))
}The handler only translates between HTTP and your domain. All decisions live in the service.
#Principles to remember
- One error type (
ApiError) for the whole app, converted withFrom. - Configuration from the environment, never hard-coded secrets.
- Dependency direction: handlers depend on services, services on repositories, never the other way around.
- Version your API (
/api/v1) from day one. - Prefer small modules over giant files. Structure follows features.
Start simple and refactor into layers when a file starts doing too many things. The goal is code where each part has one job.
Common Mistakes
Anticipate and avoid these frequent pitfalls when working with this concept.
Putting SQL directly in handlers
It is the fastest way to get a feature working.
Explanation: Moving data access into a repository lets you reuse it, test it separately and change the database without touching handlers.
Hard-coding configuration
Values like ports and database URLs change between environments.
Explanation: Read configuration from environment variables so the same binary runs in development and production.
Practical Exercise
Sketch your layers
TaskFor a "notes" API, list the files you would create for handlers, service, repository and models.
Requirements
- One file per layer
- A single place that registers routes
Need a hint?
Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.
Real-World Application
Teams that follow layered structure can swap databases, add a message queue or test business rules without starting a web server, because the layers are independent.
Key Takeaways
- Handlers handle HTTP, services hold business logic, repositories hold SQL.
- Keep main.rs small: configuration, state and server start only.
- Use one ApiError type and environment-based configuration.
- Organize by feature and register routes with configure().
Finished reading?
Mark this lesson as done to track your progress and earn XP.