toni
Getting Started

Quick Start

Build a working Toni application end-to-end in minutes.

This guide walks through building a small REST API with a user resource. By the end you'll have a running server with a controller, a service, and proper module wiring.

Project setup

toni new users_api
cd users_api

Or add to Cargo.toml manually (see Installation).

1. Define a service

Services hold business logic. The #[injectable] macro registers the struct with the DI container so it can be injected into controllers and other services.

use toni::injectable;
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize)]
pub struct User {
    pub id: u32,
    pub name: String,
    pub email: String,
}

#[injectable(pub struct UserService {})]
impl UserService {
    pub fn new() -> Self {
        Self {}
    }

    pub fn find_all(&self) -> Vec<User> {
        vec![
            User { id: 1, name: "Alice".into(), email: "alice@example.com".into() },
            User { id: 2, name: "Bob".into(), email: "bob@example.com".into() },
        ]
    }

    pub fn find_one(&self, id: u32) -> Option<User> {
        self.find_all().into_iter().find(|u| u.id == id)
    }
}

2. Define a controller

Controllers map HTTP routes to methods. The #[controller] macro takes a base path, then each method is annotated with its HTTP verb and sub-path.

use toni::{controller, extractors::{Json, Path}, HttpResponse};
use serde::Deserialize;

#[derive(Deserialize)]
pub struct CreateUserDto {
    pub name: String,
    pub email: String,
}

#[controller("/users", pub struct UserController {
    #[inject]
    service: UserService,
})]
impl UserController {
    pub fn new(service: UserService) -> Self {
        Self { service }
    }

    #[get("")]
    fn find_all(&self) -> HttpResponse {
        let users = self.service.find_all();
        HttpResponse::ok().json(users).build()
    }

    #[get("/:id")]
    fn find_one(&self, Path(id): Path<u32>) -> HttpResponse {
        match self.service.find_one(id) {
            Some(user) => HttpResponse::ok().json(user).build(),
            None => HttpResponse::not_found().build(),
        }
    }

    #[post("")]
    fn create(&self, Json(dto): Json<CreateUserDto>) -> HttpResponse {
        HttpResponse::created()
            .json(serde_json::json!({ "name": dto.name, "email": dto.email }))
            .build()
    }
}

3. Define a module

Modules declare which controllers and services belong together. Use #[module] with controllers and providers lists.

use toni::module;

#[module(
    controllers: [UserController],
    providers: [UserService],
)]
pub struct UserModule;

4. Wire up the root module

Every Toni application has a root module. Import feature modules here.

#[module(
    imports: [UserModule],
)]
pub struct AppModule;

5. Start the server

use toni::ToniFactory;
use toni_axum::AxumAdapter;

#[tokio::main]
async fn main() {
    let mut app = ToniFactory::create(AppModule, AxumAdapter::new()).await;
    app.listen(3000, "127.0.0.1").await;
}

Run it:

cargo run

Your API is now live:

curl http://localhost:3000/users
# [{"id":1,"name":"Alice","email":"alice@example.com"},...]

curl http://localhost:3000/users/1
# {"id":1,"name":"Alice","email":"alice@example.com"}

curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Carol","email":"carol@example.com"}'
# {"name":"Carol","email":"carol@example.com"}

What happens at startup

When ToniFactory::create is called, Toni:

  1. Scans the module tree starting from AppModule
  2. Builds a dependency graph from all declared providers and controllers
  3. Instantiates everything in dependency order
  4. Runs on_module_init hooks on all providers
  5. Runs on_application_bootstrap hooks
  6. Registers all routes with the HTTP adapter

By the time listen is called, every service is fully initialized and the route tree is ready.

Next steps

On this page