Expand description
Build full-stack web applications on the Internet Computer with file-based
routing conventions familiar from Next.js and SvelteKit — but in Rust,
compiled to a single canister. Drop a handler file into src/routes/,
deploy, and your endpoint is live with automatic response certification,
typed parameters, scoped middleware, and configurable security headers.
§Features
- File-based routing —
src/routes/maps directly to URL paths. Dynamic segments (_postId/), catch-all wildcards (all.rs), dotted filenames (og.png.rs→/og.png), and nested directories all work out of the box. Seebuild::generate_routes. - IC response certification — responses are automatically certified so boundary nodes can verify them. Choose from three certification modes (Skip, ResponseOnly, Full) per route or per asset. See Certification Modes below.
- Typed route context — handlers receive a
RouteContextwith typed path params, typed search params, headers, body, and the full URL. - Scoped middleware — place a
middleware.rsin any directory to wrap all handlers below it. Middleware composes from root to leaf. Seemiddleware::MiddlewareFn. - Catch-all wildcards — name a file
all.rsto capture the remaining path. The matched tail is available asctx.wildcard. - Custom 404 handler — place a
not_found.rsat the routes root to serve a styled error page instead of the default plain-text 404. - Security headers — choose from
SecurityHeaders::strict,SecurityHeaders::permissive, orSecurityHeaders::nonepresets, or configure individual headers. - Cache control & TTL — set
Cache-Controlper asset type, configure TTL-based expiry viaCacheConfig, and invalidate cached responses withinvalidate_path,invalidate_prefix, orinvalidate_all_dynamic.
§Quick Start
1. Build script — scans src/routes/ and generates the route tree:
// build.rs
fn main() {
ic_asset_router::build::generate_routes();
}2. Route handler — a file in src/routes/ with public get, post,
etc. functions:
// src/routes/index.rs
use ic_asset_router::{HttpResponse, RouteContext, StatusCode};
use std::borrow::Cow;
pub fn get(_ctx: RouteContext<()>) -> HttpResponse<'static> {
HttpResponse::builder()
.with_status_code(StatusCode::OK)
.with_headers(vec![(
"content-type".to_string(),
"text/html; charset=utf-8".to_string(),
)])
.with_body(Cow::<[u8]>::Owned(b"<h1>Hello from the IC!</h1>".to_vec()))
.build()
}3. Canister wiring — include the generated route tree and expose the HTTP interface:
// src/lib.rs
mod route_tree {
include!(concat!(env!("OUT_DIR"), "/__route_tree.rs"));
}
fn setup() {
route_tree::ROUTES.with(|routes| {
ic_asset_router::setup(routes).build();
});
}See the examples/
directory for complete, deployable canister projects including a
React SPA
with TanStack Router/Query and per-route SEO meta tags.
§Route Handlers
Each .rs file in src/routes/ is a route handler. Export one or more
public functions named after HTTP methods and the build script wires them
to the matching URL path automatically.
§Supported Methods
Export any combination of get, post, put, patch, delete, head,
or options as pub fn from a single file. Private functions are ignored.
A file with no recognized public method function causes a build error.
§Handler Signature
Every handler receives a RouteContext and returns an
HttpResponse<'static>. All types are re-exported from
ic_asset_router — no need to depend on ic_http_certification directly:
use ic_asset_router::{HttpResponse, RouteContext, StatusCode};
use std::borrow::Cow;
pub fn get(_ctx: RouteContext<()>) -> HttpResponse<'static> {
HttpResponse::builder()
.with_status_code(StatusCode::OK)
.with_headers(vec![("content-type".into(), "text/plain".into())])
.with_body(Cow::<[u8]>::Owned(b"Hello!".to_vec()))
.build()
}The type parameter P in RouteContext<P> is the typed params struct
generated by the build script for routes with dynamic segments. Use ()
for routes without dynamic segments.
§Multiple Methods in One File
A single file can handle several HTTP methods. The library returns
405 Method Not Allowed with a correct Allow header for methods
that exist at the same path but weren’t requested:
// src/routes/items/_itemId/index.rs
use ic_asset_router::{HttpResponse, RouteContext, StatusCode};
pub fn get(ctx: RouteContext<Params>) -> HttpResponse<'static> {
// GET /items/:itemId — retrieve
}
pub fn put(ctx: RouteContext<Params>) -> HttpResponse<'static> {
// PUT /items/:itemId — update
}
pub fn delete(ctx: RouteContext<Params>) -> HttpResponse<'static> {
// DELETE /items/:itemId — delete
}
use super::Params; // generated: pub struct Params { pub item_id: String }§What RouteContext Provides
Handlers receive all request data through the context object:
ctx.params— typed path parameters (e.g.ctx.params.post_id)ctx.search— typed search (query string) params (default())ctx.query— untyped query params (HashMap<String, String>)ctx.method— HTTP methodctx.headers— request headersctx.body— raw request body bytesctx.url— full request URLctx.wildcard— catch-all wildcard tail (Option<String>)
Convenience methods: ctx.header(),
ctx.body_to_str(),
ctx.json::<T>(),
ctx.form::<T>(),
ctx.form_data().
See the json-api
example for a complete REST API with GET, POST, PUT, and DELETE.
§Middleware
Place a middleware.rs file in any directory under src/routes/ and it
wraps every handler in that directory and all subdirectories below it.
The file must export a pub fn middleware with the
MiddlewareFn signature:
use ic_asset_router::{HttpRequest, HttpResponse, RouteParams};
pub fn middleware(
req: HttpRequest,
params: &RouteParams,
next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
) -> HttpResponse<'static> {
// Before: inspect or modify the request
let response = next(req, params);
// After: inspect or modify the response
response
}Middleware can:
- Modify the request —
reqis owned; construct or alter it before passing tonext. - Modify the response — capture the return value of
nextand transform headers, body, or status before returning. - Short-circuit — return a response without calling
nextat all (e.g. return 401 for unauthorized requests). The handler never executes.
§Composition Order
Middleware at different directory levels composes automatically in
root-to-leaf order. For a request to /api/v2/data:
root middleware → /api middleware → /api/v2 middleware → handlerOn the way back, responses unwind in reverse (onion model). Only one
middleware per directory is allowed. Middleware also wraps the custom
404 handler — root-level middleware runs before not_found.rs.
§Example: CORS Middleware
use ic_asset_router::{HttpRequest, HttpResponse, RouteParams, StatusCode};
pub fn middleware(
req: HttpRequest,
params: &RouteParams,
next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
) -> HttpResponse<'static> {
let cors_headers = vec![
("access-control-allow-origin".into(), "*".into()),
("access-control-allow-methods".into(), "GET, POST, PUT, DELETE, OPTIONS".into()),
("access-control-allow-headers".into(), "content-type".into()),
];
// Short-circuit: respond to OPTIONS preflight without running the handler
if req.method().as_str() == "OPTIONS" {
return HttpResponse::builder()
.with_status_code(StatusCode::NO_CONTENT)
.with_headers(cors_headers)
.build();
}
let response = next(req, params);
// Append CORS headers to the response
let mut headers = response.headers().to_vec();
headers.extend(cors_headers);
HttpResponse::builder()
.with_status_code(response.status_code())
.with_headers(headers)
.with_body(response.body().to_vec())
.build()
}See the json-api
example for a working CORS middleware.
§Certification Modes
Every response served by an IC canister can be cryptographically certified so that boundary nodes can verify it was not tampered with. This library supports three certification modes, configurable per-route or per-asset:
§Choosing a Mode
Start with Response-Only (the default). It is correct for 90% of routes and requires zero configuration.
| Mode | When to use | Example routes |
|---|---|---|
| Response-only | Same URL always returns same content | Static pages, blog posts, docs |
| Skip | Tampering has no security impact | Health checks, /ping |
| Skip + handler auth | Fast auth-gated API (query-path perf) | /api/customers, /api/me |
| Authenticated | Response depends on caller identity, must be tamper-proof | User profiles, dashboards |
| Custom (Full) | Response depends on specific headers/params | Content negotiation, pagination |
§Response-Only (Default)
No attribute needed — just write your handler:
pub fn get(_ctx: RouteContext<()>) -> HttpResponse<'static> {
// Automatically uses ResponseOnly certification
HttpResponse::builder()
.with_status_code(StatusCode::OK)
.with_body(b"Hello!" as &[u8])
.build()
}The response body, status code, and headers are certified. The request details are not included in the hash. This is sufficient when the response depends only on the URL path and canister state.
§Skip Certification
#[route(certification = "skip")]
pub fn get(_ctx: RouteContext<()>) -> HttpResponse<'static> {
// Handler runs on every query call — like a candid query
HttpResponse::builder()
.with_body(b"{\"status\":\"ok\"}" as &[u8])
.build()
}Handler execution: Skip-mode routes run the handler on every query
call, just like candid query calls. This makes them ideal for
auth-gated API endpoints — combine with handler-level auth (JWT
validation, ic_cdk::caller() checks) for fast (~200ms) authenticated
queries without waiting for consensus (~2s update calls).
Security note: Skip certification provides the same trust level as candid query calls — both trust the responding replica without cryptographic verification by the boundary node. If candid queries are acceptable for your application, skip certification is equally acceptable.
§Skip + Handler Auth Pattern
#[route(certification = "skip")]
pub fn get(ctx: RouteContext<()>) -> HttpResponse<'static> {
let caller = ic_cdk::caller();
if caller == Principal::anonymous() {
return HttpResponse::builder()
.with_status_code(StatusCode::UNAUTHORIZED)
.with_body(b"unauthorized" as &[u8])
.build();
}
// Return caller-specific data
HttpResponse::builder()
.with_body(format!("hello {caller}").into_bytes())
.build()
}See the api-authentication
example for a complete demonstration of both patterns.
§Authenticated (Full Certification Preset)
#[route(certification = "authenticated")]
pub fn get(_ctx: RouteContext<()>) -> HttpResponse<'static> {
// Authorization header is included in certification
// User A cannot receive User B's cached response
HttpResponse::builder()
.with_body(b"{\"name\":\"Alice\"}" as &[u8])
.build()
}The authenticated preset is a preconfigured Full mode that includes
the Authorization request header and Content-Type response header.
§Custom Full Certification
#[route(certification = custom(
request_headers = ["accept"],
query_params = ["page", "limit"]
))]
pub fn get(_ctx: RouteContext<()>) -> HttpResponse<'static> {
// Each combination of Accept + page + limit is independently certified
HttpResponse::builder()
.with_body(b"page content" as &[u8])
.build()
}§Setup with Static Assets
Configure and certify assets in a single builder chain during
init/post_upgrade:
use ic_asset_router::CertificationMode;
route_tree::ROUTES.with(|routes| {
ic_asset_router::setup(routes)
.with_assets(&STATIC_DIR) // response-only (default)
.with_assets_certified(&PUBLIC_DIR, CertificationMode::skip()) // skip
.build();
});§Performance Comparison
| Mode | Relative cost | Witness size |
|---|---|---|
| Skip | ~0 | Minimal |
| Response-only | Low | ~200 bytes |
| Full (authenticated) | Medium | ~300 bytes |
| Full (custom) | Medium–High | ~300–500 bytes |
§Security Model: Certification vs Candid Calls
IC canisters support two HTTP interfaces and two candid call types, each with different trust assumptions:
| Mechanism | Consensus | Boundary node verifies? | Trust model |
|---|---|---|---|
| Candid update call | Yes (2s) | N/A | Consensus — response reflects agreed-upon state |
| Candid query call | No (200ms) | No | Trust the replica |
| HTTP + ResponseOnly/Full cert | Yes (2s) | Yes | Consensus — boundary node verifies the certificate |
| HTTP + Skip cert | No (200ms) | No | Trust the replica |
Key insight: Skip certification and candid query calls have the same trust model. Both execute on a single replica without consensus, and neither response is cryptographically verified. If your application already uses candid queries (as most IC apps do), skip certification is equally acceptable for equivalent operations.
§Common Mistakes
- Over-certifying: Certifying
User-Agentcauses cache fragmentation (every browser version gets a separate certificate). Only certify headers that affect the response content. - Under-certifying: Using response-only for authenticated endpoints
means a malicious replica can serve any cached response to any user.
Use
#[route(certification = "authenticated")]instead. - Certifying non-deterministic data: If the response body changes
every call (e.g., timestamps), the certificate is immediately stale.
Use
skipor add a TTL.
Re-exports§
pub use assets::delete_assets;pub use assets::invalidate_all_dynamic;pub use assets::invalidate_path;pub use assets::invalidate_prefix;pub use assets::last_certified_at;pub use certification::CertificationMode;pub use certification::FullConfig;pub use certification::FullConfigBuilder;pub use certification::ResponseOnlyConfig;pub use config::AssetConfig;pub use config::CacheConfig;pub use config::CacheControl;pub use config::SecurityHeaders;pub use context::deserialize_search_params;pub use context::parse_form_body;pub use context::parse_query;pub use context::url_decode;pub use context::FormBodyError;pub use context::JsonBodyError;pub use context::QueryParams;pub use context::RouteContext;pub use route_config::RouteConfig;pub use router::HandlerResult;pub use router::RouteParams;
Modules§
- asset_
router - Custom asset router with per-asset certification modes.
- assets
- Static and dynamic asset certification, invalidation, and serving helpers.
- build
- Build-script utilities for file-based route generation.
- certification
- Certification mode configuration types. Certification mode configuration for HTTP responses.
- config
- Global configuration types: security headers, cache control, TTL settings.
- context
- Request context types passed to route handlers.
- middleware
- Middleware type definition.
- mime
- MIME type detection from file extensions.
- route_
config - Per-route configuration types (certification mode, TTL, headers).
- router
- Route trie, handler types, and dispatch logic.
Structs§
- Http
Request - A Candid-encodable representation of an HTTP request. This struct is used by
the
http_requestmethod of the HTTP Gateway Protocol’s Candid interface. - Http
Request Options - Options controlling the behavior of
http_request. - Http
Response - A Candid-encodable representation of an HTTP response. This struct is used
by the
http_requestmethod of the HTTP Gateway Protocol’s Candid interface. - Method
- The Request Method (VERB)
- Setup
Builder - Builder for canister initialization. Created by
setup(). - Status
Code - An HTTP status code (
status-codein RFC 9110 et al.).
Functions§
- http_
request - Handle an HTTP query-path request.
- http_
request_ update - Handle an HTTP update-path request.
- setup
- Entry point for canister initialization. Returns a
SetupBuilderthat configures the asset router, certifies assets, and registers skip routes in a single fluent chain.
Attribute Macros§
- route
- Attribute macro for per-route certification configuration.