Micro-Framework Guide
A lightweight, Actix-style API for building web applications without the full module/controller system.
Table of Contents
- Overview
- Features
- Quick Start
- Routing
- Middleware
- State Management
- Scopes
- Built-in Middleware
- Configuration
- Best Practices
- When to Use
- API Reference
- Summary
Overview
The micro-framework provides a minimal, function-based API for building HTTP services. It's ideal for:
- Microservices
- Simple APIs
- Prototyping
- Learning Armature
- Projects that don't need DI or decorators
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Micro-Framework Mode โ
โ โ
โ App::new() โ
โ .data(State::new()) // Shared state โ
โ .wrap(Logger::default()) // Middleware โ
โ .route("/", get(index)) // Simple routes โ
โ .service( // Resource groups โ
โ scope("/api") โ
โ .route("/users", get(list_users)) โ
โ .route("/users/:id", get(get_user)) โ
โ ) โ
โ .run("0.0.0.0:8080") โ
โ .await?; โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Features
- โ Fluent builder API
- โ Function-based handlers
- โ
Path parameters (
:id,:name) - โ Query string parsing
- โ
Shared state via
Data<T> - โ Composable middleware
- โ Route scoping/grouping
- โ Built-in CORS, Logger, Compress
- โ JSON request/response helpers
- โ Zero-cost when not used
Quick Start
Minimal Example
use armature_core::micro::*;
use armature_core::{Error, HttpRequest, HttpResponse};
async fn hello(_req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok().with_body(b"Hello, World!".to_vec()))
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
App::new()
.route("/", get(hello))
.run("127.0.0.1:8080")
.await
}
JSON API Example
use armature_core::micro::*;
use armature_core::{Error, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
async fn get_user(req: HttpRequest) -> Result<HttpResponse, Error> {
let id: u64 = req.param("id")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
HttpResponse::json(&User {
id,
name: "Alice".to_string(),
})
}
async fn list_users(_req: HttpRequest) -> Result<HttpResponse, Error> {
HttpResponse::json(&vec![
User { id: 1, name: "Alice".to_string() },
User { id: 2, name: "Bob".to_string() },
])
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
App::new()
.wrap(Logger::default())
.wrap(Cors::permissive())
.route("/users", get(list_users))
.route("/users/:id", get(get_user))
.run("0.0.0.0:3000")
.await
}
Routing
Method Helpers
use armature_core::micro::*;
App::new()
.route("/", get(index))
.route("/users", get(list).post(create))
.route("/users/:id", get(show).put(update).delete(destroy))
.route("/any-method", any(catch_all))
Available helpers:
get(handler)- GET requestspost(handler)- POST requestsput(handler)- PUT requestsdelete(handler)- DELETE requestspatch(handler)- PATCH requestshead(handler)- HEAD requestsoptions(handler)- OPTIONS requestsany(handler)- All methods
Chaining Methods
Handle multiple methods on the same path:
.route("/resource",
get(read_resource)
.post(create_resource)
.put(update_resource)
.delete(delete_resource)
)
Path Parameters
async fn get_user(req: HttpRequest) -> Result<HttpResponse, Error> {
// Extract :id from /users/:id
let id = req.param("id").unwrap();
// Multiple params: /users/:user_id/posts/:post_id
let user_id = req.param("user_id").unwrap();
let post_id = req.param("post_id").unwrap();
Ok(HttpResponse::ok())
}
App::new()
.route("/users/:id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_post))
Query Parameters
async fn search(req: HttpRequest) -> Result<HttpResponse, Error> {
// GET /search?q=rust&page=1
let query = req.query("q").unwrap_or(&"".to_string());
let page = req.query("page")
.and_then(|p| p.parse::<u32>().ok())
.unwrap_or(1);
HttpResponse::json(&SearchResults { query, page })
}
Middleware
Adding Middleware
Middleware wraps handlers and can modify requests/responses:
App::new()
.wrap(Logger::default()) // Outermost - runs first
.wrap(Cors::permissive()) // Runs second
.wrap(Compress::default()) // Innermost - runs last
.route("/", get(handler))
Custom Middleware
use armature_core::micro::*;
use std::pin::Pin;
use std::future::Future;
struct Timing;
impl Middleware for Timing {
fn call(
&self,
req: HttpRequest,
next: Next,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>> {
Box::pin(async move {
let start = std::time::Instant::now();
// Call next handler in chain
let mut response = next(req).await?;
// Add timing header
response.headers.insert(
"X-Response-Time".to_string(),
format!("{}ms", start.elapsed().as_millis()),
);
Ok(response)
})
}
}
App::new()
.wrap(Timing)
.route("/", get(handler))
Middleware Order
Middleware executes in the order added (first added = outermost):
Request โ Logger โ Cors โ Compress โ Handler
โ
Response โ Logger โ Cors โ Compress โ Response
State Management
Sharing State
Use Data<T> to share state across handlers:
use armature_core::micro::*;
use std::sync::atomic::{AtomicU64, Ordering};
struct AppState {
request_count: AtomicU64,
db_pool: Pool,
}
async fn handler(req: HttpRequest) -> Result<HttpResponse, Error> {
// Access state from request extensions
// (State is automatically injected)
Ok(HttpResponse::ok())
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let state = AppState {
request_count: AtomicU64::new(0),
db_pool: create_pool().await,
};
App::new()
.data(state) // Register state
.route("/", get(handler))
.run("0.0.0.0:8080")
.await
}
Multiple State Types
#[derive(Clone)]
struct DbPool { /* ... */ }
#[derive(Clone)]
struct Config {
api_key: String,
}
App::new()
.data(DbPool::new())
.data(Config { api_key: "secret".into() })
.route("/", get(handler))
Scopes
Group routes under a common prefix:
Basic Scopes
App::new()
.service(
scope("/api/v1")
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).delete(delete_user))
.route("/posts", get(list_posts))
)
.route("/health", get(health_check))
Routes created:
GET /api/v1/usersPOST /api/v1/usersGET /api/v1/users/:idDELETE /api/v1/users/:idGET /api/v1/postsGET /health
Nested Scopes
App::new()
.service(
scope("/api")
.service(
scope("/v1")
.route("/users", get(v1_users))
)
.service(
scope("/v2")
.route("/users", get(v2_users))
)
)
Routes:
GET /api/v1/usersGET /api/v2/users
Scoped Middleware
App::new()
.wrap(Logger::default()) // Global middleware
.service(
scope("/api")
.wrap(auth_middleware) // Only for /api/*
.route("/users", get(users))
)
.route("/public", get(public_page)) // No auth required
Built-in Middleware
Logger
Logs requests with timing information:
use armature_core::micro::{Logger, LogFormat};
// Default format
App::new().wrap(Logger::default())
// Custom format
App::new().wrap(Logger::new(LogFormat::Combined))
Output:
INFO Request completed method=GET path=/users status=200 duration_ms=5
CORS
Cross-Origin Resource Sharing configuration:
use armature_core::micro::Cors;
// Permissive (allow all)
App::new().wrap(Cors::permissive())
// Custom configuration
App::new().wrap(
Cors::default()
.allowed_origins(["https://example.com", "https://app.example.com"])
.allowed_methods(["GET", "POST", "PUT", "DELETE"])
.allowed_headers(["Content-Type", "Authorization"])
.allow_credentials(true)
.max_age(3600)
)
Compress
Adds compression headers:
use armature_core::micro::{Compress, CompressionLevel};
App::new().wrap(Compress::default())
App::new().wrap(Compress::new(CompressionLevel::Best))
Configuration
Default Service (404 Handler)
async fn not_found(_req: HttpRequest) -> Result<HttpResponse, Error> {
HttpResponse::json(&serde_json::json!({
"error": "Not Found",
"message": "The requested resource does not exist"
}))
.map(|r| r.status(404))
}
App::new()
.route("/", get(index))
.default_service(not_found)
Building vs Running
// Build without starting server
let app = App::new()
.route("/", get(handler))
.build();
// Handle requests manually (useful for testing)
let response = app.handle(request).await?;
// Or run the server
App::new()
.route("/", get(handler))
.run("0.0.0.0:8080")
.await?;
Best Practices
1. Use Scopes for API Versioning
App::new()
.service(scope("/api/v1").route("/users", get(v1::users)))
.service(scope("/api/v2").route("/users", get(v2::users)))
2. Register Middleware in Correct Order
App::new()
.wrap(Logger::default()) // Log all requests (even errors)
.wrap(Cors::permissive()) // Handle CORS before auth
.wrap(auth_middleware) // Auth after CORS preflight
3. Use Typed State
// โ
Good - typed state
#[derive(Clone)]
struct AppState { db: Pool }
// โ Avoid - untyped data
.data(HashMap::new())
4. Handle Errors Properly
async fn handler(req: HttpRequest) -> Result<HttpResponse, Error> {
let id = req.param("id")
.ok_or_else(|| Error::validation("Missing id parameter"))?;
let id: u64 = id.parse()
.map_err(|_| Error::validation("Invalid id format"))?;
// ...
}
5. Keep Handlers Small
// โ
Good - delegate to service layer
async fn create_user(req: HttpRequest) -> Result<HttpResponse, Error> {
let body: CreateUserRequest = req.json()?;
let user = user_service::create(body).await?;
HttpResponse::json(&user)
}
// โ Avoid - business logic in handler
async fn create_user(req: HttpRequest) -> Result<HttpResponse, Error> {
// 100+ lines of database calls, validation, etc.
}
When to Use
Use Micro-Framework When
- โ Building microservices
- โ Simple REST APIs
- โ Quick prototypes
- โ Learning Armature
- โ Performance-critical services
- โ Don't need dependency injection
- โ Prefer explicit over implicit
Use Full Framework When
- โ Large enterprise applications
- โ Need dependency injection
- โ Want decorator-based controllers
- โ Complex middleware requirements
- โ GraphQL subscriptions
- โ Automatic OpenAPI generation
Comparison
| Aspect | Micro-Framework | Full Framework |
|---|---|---|
| Setup | App::new() |
Application::bootstrap(Module) |
| Routing | get(handler) |
@Get() decorator |
| DI | Manual Data<T> |
@Injectable auto-wiring |
| Middleware | wrap(mw) |
@UseGuards, @UsePipes |
| Best for | Microservices | Enterprise apps |
API Reference
App
impl App {
fn new() -> Self;
fn data<T: Clone + Send + Sync + 'static>(self, data: T) -> Self;
fn wrap<M: Middleware + 'static>(self, middleware: M) -> Self;
fn route(self, path: &str, route: RouteBuilder) -> Self;
fn service(self, scope: Scope) -> Self;
fn default_service<H>(self, handler: H) -> Self;
fn build(self) -> BuiltApp;
async fn run(self, addr: impl ToSocketAddrs) -> std::io::Result<()>;
}
RouteBuilder
fn get<H>(handler: H) -> RouteBuilder;
fn post<H>(handler: H) -> RouteBuilder;
fn put<H>(handler: H) -> RouteBuilder;
fn delete<H>(handler: H) -> RouteBuilder;
fn patch<H>(handler: H) -> RouteBuilder;
fn head<H>(handler: H) -> RouteBuilder;
fn options<H>(handler: H) -> RouteBuilder;
fn any<H>(handler: H) -> RouteBuilder;
Scope
fn scope(prefix: impl Into<String>) -> Scope;
impl Scope {
fn route(self, path: &str, route: RouteBuilder) -> Self;
fn wrap<M: Middleware + 'static>(self, middleware: M) -> Self;
fn service(self, inner: Scope) -> Self;
}
Data
impl<T> Data<T> {
fn new(data: T) -> Self;
fn get_ref(&self) -> &T;
fn into_inner(self) -> Arc<T>;
}
impl<T> Deref for Data<T> {
type Target = T;
}
Summary
The micro-framework provides a lightweight, Actix-style API for building web applications:
Key Points:
- Use
App::new()to create applications - Register routes with
get(),post(), etc. - Share state with
.data()andData<T> - Add middleware with
.wrap() - Group routes with
scope() - Run with
.run("addr").await
Performance:
- Empty app creation: ~25ns
- Route matching: ~600-900ns
- State access: <1ns
Best For:
- Microservices
- Simple APIs
- Learning Armature
- Performance-critical code
For complex applications needing DI and decorators, use the full framework mode instead.