What Is Astrea?
Astrea turns your file structure into API routes — at compile time, with zero runtime cost. Drop a .rs file into the src/routes/ folder, and it becomes an HTTP endpoint. No manual route registration, no build.rs, no boilerplate.
Every handler looks the same:
async
That's it. No complex extractor signatures. No learning curve for each parameter type.
Features
- 📁 File-based routing — file name = route path, generated at compile time
- 🎯 Unified handler signature — every handler is
async fn(Event) -> Result<Response> - 🔧 Simple extractors —
get_param(),get_query_param(),get_body()— just call a function - 🧅 Scoped middleware —
_middleware.rsfiles with inherit (extend) or replace (override) modes - 📝 OpenAPI auto-gen — optional Swagger UI + OpenAPI 3.0 spec from your code (feature flag
openapi) - 🔄 Axum compatible — works with all existing Axum middleware and the Tower ecosystem
- 📦 Zero extra deps — re-exports
axum,tokio,serde,tower, etc. — just depend onastrea
Quick Start
1. Create a new project
2. Add Astrea
Or in Cargo.toml:
[]
= "my-api"
= "2024"
[]
= "0.0.1"
Note: Astrea requires Rust edition 2024 (Rust ≥ 1.85).
3. Create your route files
my-api/
├── src/
│ ├── main.rs
│ └── routes/
│ ├── index.get.rs # GET /
│ └── users/
│ ├── index.get.rs # GET /users
│ ├── index.post.rs # POST /users
│ └── [id].get.rs # GET /users/:id
src/routes/index.get.rs
use *;
pub async
src/routes/users/[id].get.rs
use *;
pub async
4. Write main.rs
async
5. Run
Done. You will see a beautiful startup log:
┌─────────────────────────────────────────────────────────────────────┐
│ 🚀 Astrea Router │
├────────┬──────────────────────────────┬─────────────────────────────┤
│ Method │ Path │ Middleware │
├────────┼──────────────────────────────┼─────────────────────────────┤
│ GET │ / │ (none) │
│ GET │ /users │ (none) │
│ POST │ /users │ (none) │
│ GET │ /users/:id │ (none) │
└────────┴──────────────────────────────┴─────────────────────────────┘
✅ 4 route(s), 0 middleware scope(s) loaded
And GET http://localhost:3000/ returns {"message":"Hello, World!"}.
Route File Naming Convention
| File name | Route |
|---|---|
src/routes/index.get.rs |
GET / |
src/routes/users.get.rs |
GET /users |
src/routes/users/index.post.rs |
POST /users |
src/routes/users/[id].get.rs |
GET /users/:id |
src/routes/users/[id].delete.rs |
DELETE /users/:id |
src/routes/posts/[...slug].get.rs |
GET /posts/*slug (catch-all) |
Rules:
- File name format:
<name>.<method>.rs indexis a special name — it maps to the directory itself (no extra path segment)[param]→ dynamic path parameter[...param]→ catch-all parameter (matches everything after)
Extracting Request Data
Astrea replaces complex Axum extractor signatures with simple function calls:
pub async
Response Helpers
// JSON (application/json)
json?
// Plain text (text/plain)
text
// HTML (text/html)
html
// Redirect (302 Found)
redirect?
// No Content (204)
no_content
// Raw bytes
bytes.content_type
// Streaming
stream
All responses support chaining:
json?
.status
.header
Error Handling
Return errors naturally — they become proper HTTP responses automatically:
pub async
Built-in error variants:
| Method | Status Code |
|---|---|
RouteError::bad_request(msg) |
400 |
RouteError::unauthorized(msg) |
401 |
RouteError::forbidden(msg) |
403 |
RouteError::not_found(msg) |
404 |
RouteError::conflict(msg) |
409 |
RouteError::validation(msg) |
422 |
RouteError::rate_limit(msg) |
429 |
RouteError::custom(StatusCode, msg) |
any |
? on any anyhow-compatible error |
500 |
All errors are returned as JSON: {"error": "...", "status": 404}.
Middleware
Create _middleware.rs files anywhere in the src/routes/ directory. They scope to the folder they live in + all subfolders.
src/routes/
├── _middleware.rs # applies to ALL routes
├── api/
│ ├── _middleware.rs # applies to /api/* (stacks on root)
│ ├── users.get.rs # ← root + api middleware
│ └── public/
│ ├── _middleware.rs # OVERRIDES parent middleware
│ └── health.get.rs # ← public middleware only
Extend mode (default) — stack on parent
// src/routes/_middleware.rs
use *;
Override mode — replace parent middleware
// src/routes/api/public/_middleware.rs
use *;
OpenAPI (Optional)
Enable the openapi feature to get automatic API documentation:
[]
= { = "0.0.1", = ["openapi"] }
Then merge the OpenAPI router:
let app = create_router
.merge;
This gives you:
GET /openapi.json— the OpenAPI 3.0 specGET /swagger— Swagger UI
Application State
Share state across handlers (database pools, config, etc.):
// In handler:
pub async
Full Example
my-api/
├── Cargo.toml
└── src/
├── main.rs
└── routes/
├── _middleware.rs
├── index.get.rs
└── api/
├── _middleware.rs
├── users.get.rs
├── users.post.rs
└── users/
├── [id].get.rs
├── [id].put.rs
└── [id].delete.rs
This generates:
GET /— root pageGET /api/users— list usersPOST /api/users— create userGET /api/users/:id— get user by IDPUT /api/users/:id— update userDELETE /api/users/:id— delete user
Root middleware → all routes. API middleware → /api/* routes.
Why Astrea?
| Astrea | Plain Axum | |
|---|---|---|
| Route definition | Drop a file | Manual .route() calls |
| Handler signature | Always (Event) -> Result<Response> |
Varies per extractor combo |
| Parameter access | get_param(&event, "id") |
Path(id): Path<String> |
| Error handling | Built-in JSON errors | DIY |
| Middleware | File-based scoping | Manual nesting |
| OpenAPI | Auto-generated | Manual or third-party |
Minimum Supported Rust Version
Rust 1.85 or later (edition 2024).
License
MIT © TNXG (Asahi Shiori)