Gearbox
A lightweight, opinionated Rust web framework with automatic dependency injection.
Features
- Dependency Injection - Declare dependencies with
#[inject]and let Gearbox wire everything together - Auto-Registration - Services (Cogs) are automatically discovered and registered at startup
- Dependency Resolution - Topological sorting ensures services are initialized in the correct order
- Configuration System - Load config from TOML files and environment variables with relaxed binding
- Route Macros - Define HTTP handlers with
#[get],#[post], etc. and automatic parameter injection - PostgreSQL Support -
#[derive(PgEntity)]generates CRUD operations,pg_queries!for custom SQL - REST API Generation -
#[derive(Crud)]generates complete REST endpoints with pagination - Built on Axum - Leverages the battle-tested Axum web framework under the hood
Quick Start
Add Gearbox to your Cargo.toml:
[]
= "0.1"
= { = "1", = ["rt-multi-thread", "macros"] }
# For PostgreSQL support:
# gearbox-rs = { version = "0.1", features = ["postgres"] }
Hello World Example
use *;
use Arc;
// Helper function for default message
// Define a service (Cog)
// Define a route that uses the service
async
// Start the application
Run with:
Visit http://localhost:8080/hello/world to see: Hello, world!
Core Concepts
Cogs (Services)
A Cog is Gearbox's term for a service or component. Use the #[cog] macro to define one:
Field attributes:
#[inject]- Inject anArc<T>from the service registry#[config]- Load from configuration (requiresCogConfigimpl)#[default(fn)]- Initialize via a sync function#[default_async(fn)]- Initialize via an async function- No attribute - Uses
Default::default()
Configuration
Define configuration structs with #[cog_config]:
Create a config.toml:
[]
= 8080
= "info"
[]
= "postgres://localhost/mydb"
= 10
Override with environment variables:
GEARBOX_DATABASE_URL=postgres://prod/mydb
GEARBOX_DATABASE_MAXCONNECTIONS=50
GEARBOX_GEARBOXAPP_HTTPPORT=3000
Gearbox uses relaxed binding - all of these match max_connections:
max-connections(TOML kebab-case)max_connections(TOML snake_case)MAXCONNECTIONS(env var)
Default Values
If a TOML section is missing entirely, Gearbox uses Default::default() for that config type. For partial sections (some fields present, others missing), use #[serde(default)] on the struct to fill in missing fields from Default:
// Missing fields use Default impl values
Config Validation
Add a validation function and reference it in the macro to catch invalid values at startup:
Validation runs automatically after deserialization during Gearbox::crank(). If validation fails, startup aborts with a clear error message.
Startup Diagnostics
Gearbox logs configuration status at startup:
- WARN for each config section not found in the file/env (falling back to defaults)
- WARN for TOML sections that exist but have no matching
#[cog_config]type (possible typos)
Routes
Define HTTP handlers with route macros:
async
async
async
Available macros: #[get], #[post], #[put], #[delete], #[patch]
Arc<T> parameters are automatically transformed to use Gearbox's Inject<T> extractor. Other Axum extractors (Json, Path, Query, etc.) work as normal.
How Parameter Injection Works
When you write a handler like this:
async
The route macro rewrites it (roughly) to:
async
Inject<T> is an axum extractor that implements FromRequestParts. At request time it:
- Extracts the
Arc<Hub>from axum's request state. - Calls
hub.registry.get::<T>()to look up the Cog by type. - Returns the
Arc<T>wrapped inInject(...).
Non-Arc parameters (Json<T>, Path<T>, Query<T>, etc.) are passed through to axum unchanged. This means you can mix injected services with standard extractors freely.
PostgreSQL Entities
Generate repository operations with #[derive(PgEntity)]:
// Generated methods on PgClient:
// - create(entity) -> INSERT
// - update(entity) -> UPDATE
// - upsert(entity) -> INSERT ... ON CONFLICT
// - find_by_id(id) -> SELECT
// - find_by_ids(ids) -> SELECT ... IN
// - find_page(limit, offset) -> SELECT with pagination
// - exists(id) -> SELECT EXISTS
// - count() -> SELECT COUNT
// - delete(id) -> DELETE
// - create_batch(entities) -> batch INSERT
// - upsert_batch(entities) -> batch UPSERT
Custom Queries
Define custom SQL queries with pg_queries!:
use pg_queries;
pg_queries!
When using multi-schema support, declare the schema at the top of the block:
pg_queries!
Usage - import the generated PgQueries trait (or PgQueries{Schema} when using a named schema):
use cratePgQueries;
let user = client.find_user_by_email.await?;
let count = client.count_users_by_role.await?;
let deleted = client.deactivate_user.await?; // returns bool
Return type mapping:
| Return Type | Behavior |
|---|---|
Option<T> |
fetch_optional - returns None if no row |
Vec<T> |
fetch_all - returns all matching rows |
T (struct) |
fetch_one - errors if no row found |
i64, String, etc. |
query_scalar - single column value |
bool |
execute - true if rows_affected > 0 |
u64 |
execute - returns rows_affected |
| (none) | execute - returns () |
Complex Joins
Use custom structs to represent JOIN results:
// Define a struct for the join result
pg_queries!
The struct field names must match the column aliases in your SQL query. Use AS to rename columns from joins to avoid conflicts and match your struct fields.
REST API Generation with Crud
Generate complete REST endpoints with #[derive(Crud)]:
This generates:
DTOs:
UserCreate- For POST requests (excludesid,created_at)UserUpdate- For PATCH requests (all fields optional)UserQuery- Query parameters for paginationUserResponse- For responses (excludespassword_hash)
Routes:
| Method | Path | Description |
|---|---|---|
| GET | /users |
List with pagination |
| GET | /users/{id} |
Get single entity |
| POST | /users |
Create new entity |
| PUT | /users/{id} |
Full update |
| PATCH | /users/{id} |
Partial update |
| DELETE | /users/{id} |
Delete entity |
Field Attributes
| Attribute | Description |
|---|---|
#[auto_generated] |
DB generates this (UUID, serial). Excluded from create/update DTOs |
#[readonly] |
Only in responses (e.g., created_at). Excluded from create/update |
#[writeonly] |
Only in create/update (e.g., password_hash). Excluded from responses |
Query Examples
# Paginate results
&offset=0
For custom filtering, use pg_queries! with a custom route handler.
Struct-Level Options
// Only GET endpoints
// No POST endpoint
// No DELETE endpoint
Multi-Schema Support
Gearbox supports multiple PostgreSQL schemas within a single database instance. This is useful for consolidating multiple apps into a single monolith while keeping their data isolated.
Configuration
Define multiple schemas in config.toml:
[]
= "localhost:5432"
= "postgres"
= "postgres"
= 5
[]
= "accounts"
= "./migrations/accounts"
[]
= "billing"
= "./migrations/billing"
Each schema gets its own connection pool with search_path pinned, and its own migration directory. Schemas are created automatically on startup.
Entity Schema Binding
Annotate entities with #[schema("name")] to bind them to a specific schema's pool:
Each entity's generated repository methods automatically route to the correct pool. Cross-schema queries are discouraged by design — each entity is bound to exactly one schema.
Custom Queries with Schemas
Each pg_queries! block declares which schema it targets:
pg_queries!
pg_queries!
Backward Compatibility
The legacy single-schema config format still works:
[]
= "my_app"
= "./migrations"
Entities without a #[schema("...")] attribute use the "default" pool, which maps to the legacy config. Existing apps require zero changes.
Middleware
Gearbox exposes Axum's middleware system through the router_with method. Use it to add tower layers for CORS, tracing, compression, auth, and more.
Adding Middleware
When you need middleware, write main manually instead of using #[gearbox_app]:
use ;
use CorsLayer;
use TraceLayer;
async
The closure receives the fully-built axum::Router (after state is applied), so any tower Layer works directly. #[gearbox_app] still works for apps that don't need middleware.
Lifecycle Hooks
Cogs can opt in to lifecycle callbacks with #[on_start] and #[on_shutdown] attributes:
on_startis called after all cogs are initialized, in dependency orderon_shutdownis called when the server receives Ctrl+C or SIGTERM, in reverse dependency order- Both are optional - cogs without these attributes have no-op defaults
See examples/03-middleware/ for a complete working example.
Dependency Resolution
Gearbox uses topological sorting (Kahn's algorithm) to determine the order in which Cogs are initialized. Each #[inject] field declares a dependency, and Cogs with no dependencies are built first.
Database (no deps) -> initialized first
|
UserRepo (#[inject] Database) -> initialized second
|
UserService (#[inject] UserRepo) -> initialized third
Cyclic dependencies are detected at startup. If Cog A depends on B and B depends on A, Gearbox::crank() returns an error:
Error: Cyclic dependency detected involving: CogA, CogB
To resolve a cycle, restructure your services to break the circular reference — for example, extract shared logic into a third Cog.
Missing dependencies are also caught at startup. If a #[inject] field references a type that has no corresponding #[cog] struct, you'll see:
Error: Missing dependency: 'UserService' requires '<TypeId>' which is not registered
Common causes: forgetting to annotate a struct with #[cog], or not including the crate that defines it.
Shutdown order is the reverse of initialization order — dependents shut down before their dependencies. Each Cog's on_shutdown hook has a 30-second timeout; timeouts are logged but do not block other Cogs from shutting down.
Project Structure
gearbox-rs/
├── core/ # Core framework (Hub, Config, DI, routing)
├── macros/ # Procedural macros (#[cog], #[get], etc.)
├── pg/ # PostgreSQL integration
└── examples/ # Example applications
License
MIT