Skip to main content

Crate ic_asset_router

Crate ic_asset_router 

Source
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 routingsrc/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. See build::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 RouteContext with typed path params, typed search params, headers, body, and the full URL.
  • Scoped middleware — place a middleware.rs in any directory to wrap all handlers below it. Middleware composes from root to leaf. See middleware::MiddlewareFn.
  • Catch-all wildcards — name a file all.rs to capture the remaining path. The matched tail is available as ctx.wildcard.
  • Custom 404 handler — place a not_found.rs at the routes root to serve a styled error page instead of the default plain-text 404.
  • Security headers — choose from SecurityHeaders::strict, SecurityHeaders::permissive, or SecurityHeaders::none presets, or configure individual headers.
  • Cache control & TTL — set Cache-Control per asset type, configure TTL-based expiry via CacheConfig, and invalidate cached responses with invalidate_path, invalidate_prefix, or invalidate_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 method
  • ctx.headers — request headers
  • ctx.body — raw request body bytes
  • ctx.url — full request URL
  • ctx.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 requestreq is owned; construct or alter it before passing to next.
  • Modify the response — capture the return value of next and transform headers, body, or status before returning.
  • Short-circuit — return a response without calling next at 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 → handler

On 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.

ModeWhen to useExample routes
Response-onlySame URL always returns same contentStatic pages, blog posts, docs
SkipTampering has no security impactHealth checks, /ping
Skip + handler authFast auth-gated API (query-path perf)/api/customers, /api/me
AuthenticatedResponse depends on caller identity, must be tamper-proofUser profiles, dashboards
Custom (Full)Response depends on specific headers/paramsContent 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

ModeRelative costWitness size
Skip~0Minimal
Response-onlyLow~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:

MechanismConsensusBoundary node verifies?Trust model
Candid update callYes (2s)N/AConsensus — response reflects agreed-upon state
Candid query callNo (200ms)NoTrust the replica
HTTP + ResponseOnly/Full certYes (2s)YesConsensus — boundary node verifies the certificate
HTTP + Skip certNo (200ms)NoTrust 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-Agent causes 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 skip or 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§

HttpRequest
A Candid-encodable representation of an HTTP request. This struct is used by the http_request method of the HTTP Gateway Protocol’s Candid interface.
HttpRequestOptions
Options controlling the behavior of http_request.
HttpResponse
A Candid-encodable representation of an HTTP response. This struct is used by the http_request method of the HTTP Gateway Protocol’s Candid interface.
Method
The Request Method (VERB)
SetupBuilder
Builder for canister initialization. Created by setup().
StatusCode
An HTTP status code (status-code in 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 SetupBuilder that 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.