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_apiOr 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 runYour 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:
- Scans the module tree starting from
AppModule - Builds a dependency graph from all declared providers and controllers
- Instantiates everything in dependency order
- Runs
on_module_inithooks on all providers - Runs
on_application_bootstraphooks - 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
- Project Structure — how to organize a larger application
- Modules — deep dive into the module system
- Controllers — all the route definition options
- Dependency Injection — how the container works