Skip to main content
Lesson 1 of 2Intermediate⏱ 15 mins

API Architecture & Structure

Structuring a large Actix Web project.

0% of path

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

code_blockstext
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:

code_blocksrust
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

code_blocksrust
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 with From.
  • 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.

Incorrect Pattern
let row = sqlx::query("SELECT ...").fetch_one(&pool).await?; // inside a handler
Correct Pattern
let user = state.users.find(id).await?; // repository does the SQL

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.

Incorrect Pattern
.bind(("127.0.0.1", 8080))?
Correct Pattern
.bind((cfg.host.as_str(), cfg.port))?

Explanation: Read configuration from environment variables so the same binary runs in development and production.

Practical Exercise

Sketch your layers

Task

For 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.