Routes
Routes map incoming HTTP requests to handler functions. FORGE organizes routes into two groups: admin routes for authenticated management endpoints and public routes for client-facing data. Routes are registered using Axum's Router and composed with middleware for authentication and authorization.
Route Organization
/api/
├── auth/
│ ├── POST login
│ ├── POST register
│ ├── POST refresh
│ ├── POST logout
│ ├── POST forgot-password
│ ├── POST reset-password
│ ├── POST send-otp
│ └── POST verify-otp
├── profile GET, PUT (authenticated)
├── contents/{slug} GET (public)
├── lookups/{type} GET (public)
├── menus/{location} GET (public)
└── admin/ (auth + permissions required)
├── users/ GET, POST
│ └── {id} GET, PUT, DELETE
│ └── roles POST
├── roles/ GET, POST
│ └── {id} GET, PUT, DELETE
├── permissions/ GET
├── contents/ GET, POST
│ └── {id} GET, PUT, DELETE
├── lookups/ GET, POST
│ └── {id} GET, PUT, DELETE
├── menus/ GET, POST
│ └── {id} GET, PUT, DELETE
└── settings/ GET, PUTRoute Registration
Main Router
The top-level router in main.rs composes all route groups:
// src/main.rs
let app = Router::new()
// Admin routes (auth + RBAC middleware applied inside)
.nest("/api/admin", routes::admin::routes(pool.clone()))
// Public API routes
.nest("/api", routes::public::routes(pool.clone()))
// Health checks
.route("/health", get(|| async { "OK" }))
.route("/ready", get(health_check))
// Documentation
.merge(SwaggerUi::new("/docs").url("/api-docs/openapi.json", ApiDoc::openapi()))
.merge(Redoc::with_url("/redoc", ApiDoc::openapi()))
// Global middleware
.layer(CorsLayer::permissive())
.layer(middleware::from_fn(middleware::request_id::add_request_id));Admin Routes
Admin routes apply authentication middleware to the entire group and permission middleware to individual routes:
// src/routes/admin.rs
use axum::{Router, routing::{get, post, put, delete}, middleware};
use crate::handlers::admin;
use crate::middleware::{auth::auth_middleware, rbac::require_permission};
pub fn routes(pool: PgPool) -> Router {
Router::new()
// User management
.route(
"/users",
get(admin::users::list)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("users.view"),
))
)
.route(
"/users",
post(admin::users::create)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("users.create"),
))
)
.route(
"/users/:id",
get(admin::users::show)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("users.view"),
))
)
.route(
"/users/:id",
put(admin::users::update)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("users.edit"),
))
)
.route(
"/users/:id",
delete(admin::users::delete)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("users.delete"),
))
)
.route(
"/users/:id/roles",
post(admin::users::assign_roles)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("roles.edit"),
))
)
// Role management
.route(
"/roles",
get(admin::roles::list)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("roles.view"),
))
)
.route(
"/roles",
post(admin::roles::create)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("roles.create"),
))
)
.route(
"/roles/:id",
put(admin::roles::update)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("roles.edit"),
))
)
.route(
"/roles/:id",
delete(admin::roles::delete)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("roles.delete"),
))
)
// Permissions (read-only)
.route(
"/permissions",
get(admin::permissions::list)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("roles.view"),
))
)
// Content management
.route(
"/contents",
get(admin::contents::list)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("contents.view"),
))
)
.route(
"/contents",
post(admin::contents::create)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("contents.create"),
))
)
.route(
"/contents/:id",
get(admin::contents::show)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("contents.view"),
))
)
.route(
"/contents/:id",
put(admin::contents::update)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("contents.edit"),
))
)
.route(
"/contents/:id",
delete(admin::contents::delete)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
require_permission("contents.delete"),
))
)
// Settings
.route("/settings", get(admin::settings::list))
.route("/settings", put(admin::settings::update))
// Apply auth middleware to all admin routes
.layer(middleware::from_fn_with_state(
pool.clone(),
auth_middleware,
))
.with_state(pool)
}WARNING
The auth_middleware is applied as a layer to the entire admin router. This means every route under /api/admin/* requires a valid JWT token. Individual routes then apply require_permission for fine-grained access control.
Public Routes
Public routes are accessible without authentication, though some may optionally use it:
// src/routes/public.rs
use axum::{Router, routing::{get, post}};
use crate::handlers;
pub fn routes(pool: PgPool) -> Router {
Router::new()
// Authentication (no auth required)
.route("/auth/login", post(handlers::auth::login))
.route("/auth/register", post(handlers::auth::register))
.route("/auth/refresh", post(handlers::auth::refresh))
.route("/auth/logout", post(handlers::auth::logout))
.route("/auth/forgot-password", post(handlers::auth::forgot_password))
.route("/auth/reset-password", post(handlers::auth::reset_password))
.route("/auth/send-otp", post(handlers::auth::send_otp))
.route("/auth/verify-otp", post(handlers::auth::verify_otp))
// Profile (auth required)
.route(
"/profile",
get(handlers::profile::show)
.put(handlers::profile::update)
.route_layer(middleware::from_fn_with_state(
pool.clone(),
auth_middleware,
))
)
// Public content (no auth required)
.route("/contents/:slug", get(handlers::contents::show))
.route("/lookups/:type", get(handlers::lookups::by_type))
.route("/menus/:location", get(handlers::menus::by_location))
.with_state(pool)
}Middleware Application
Middleware is applied at different levels depending on scope:
Global Middleware (all routes)
├── CORS
├── Request ID
│
├── /api/admin/* (admin routes)
│ ├── Auth Middleware (all admin routes)
│ │ ├── /users → require_permission("users.view")
│ │ ├── /users POST → require_permission("users.create")
│ │ └── ...
│ │
├── /api/* (public routes)
│ ├── /auth/* → No middleware
│ ├── /profile → Auth Middleware (route-level)
│ └── /contents/:slug → No middlewareTIP
Apply middleware at the narrowest possible scope. Global middleware runs on every request, including health checks and documentation endpoints. Use route_layer for route-specific middleware to keep the overhead minimal.
Route Patterns
Resource CRUD Routes
The standard pattern for a new resource:
// Add to src/routes/admin.rs
.route("/products", get(admin::products::list))
.route("/products", post(admin::products::create))
.route("/products/:id", get(admin::products::show))
.route("/products/:id", put(admin::products::update))
.route("/products/:id", delete(admin::products::delete))Nested Resource Routes
For resources that belong to a parent:
// Comments belong to a content entry
.route("/contents/:content_id/comments", get(admin::comments::list))
.route("/contents/:content_id/comments", post(admin::comments::create))
.route("/contents/:content_id/comments/:id", put(admin::comments::update))
.route("/contents/:content_id/comments/:id", delete(admin::comments::delete))Action Routes
For non-CRUD actions on a resource:
// Custom actions
.route("/users/:id/roles", post(admin::users::assign_roles))
.route("/users/:id/activate", post(admin::users::activate))
.route("/users/:id/deactivate", post(admin::users::deactivate))Path Parameters
Axum extracts path parameters using the Path extractor:
// Single parameter
.route("/users/:id", get(show_user))
async fn show_user(Path(id): Path<Uuid>) -> impl IntoResponse { /* ... */ }
// Multiple parameters
.route("/contents/:content_id/comments/:id", get(show_comment))
async fn show_comment(
Path((content_id, id)): Path<(Uuid, Uuid)>,
) -> impl IntoResponse { /* ... */ }See Also
- Handlers — The functions that routes point to
- Middleware — Auth and RBAC middleware implementation
- Authorization — Permission system used in route guards
- Overview — Full project structure