ic-asset-router
Build full-stack web applications on the Internet Computer with the same file-based routing conventions you know 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. No frontend framework required; bring your own template engine, return JSON, or serve raw HTML.
Features
- File-based routing —
src/routes/maps directly to URL paths. Dynamic segments (_postId/), catch-all wildcards (all.rs), and nested directories are all supported. - IC response certification — responses are automatically certified so boundary nodes can verify them. Choose from three certification modes (Skip, ResponseOnly, Full) per route via
#[route(certification = "...")]. See Certification Modes. - Typed route context — handlers receive a
RouteContext<P, S>with 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. - Security headers — choose from
strict,permissive, ornonepresets, or configure individual headers. - Cache control & TTL — set
Cache-Controlper asset type, configure TTL-based expiry, and invalidate cached responses on demand.
Table of Contents
- Quick Start
- Route Handlers
- Routing Conventions
- Middleware
- Certification Modes
- Configuration
- Examples
- How This Library Was Built
- Updates
- Author
- Contributing
- License
Quick Start
1. Add the dependency
ic-asset-router must appear in both [dependencies] and [build-dependencies]:
[]
= "0.10"
= "0.18"
= { = "../ic-asset-router" }
[]
= { = "../ic-asset-router" }
2. Create the build script
// build.rs
3. Write a route handler
// src/routes/index.rs
use ;
use Cow;
4. Wire up the canister
// src/lib.rs
use ;
use ;
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 these function names from a single file:
| Function | HTTP method |
|---|---|
get |
GET |
post |
POST |
put |
PUT |
patch |
PATCH |
delete |
DELETE |
head |
HEAD |
options |
OPTIONS |
Only pub fn declarations are detected — 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:
use ;
use Cow;
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 ;
use Params; // generated: pub struct Params { pub item_id: String }
What RouteContext provides
Handlers receive all request data through the context object:
| Field | Type | Description |
|---|---|---|
ctx.params |
P |
Typed path parameters (e.g. ctx.params.post_id) |
ctx.search |
S |
Typed search (query string) params (default ()) |
ctx.query |
HashMap<String, String> |
Untyped query params, always available |
ctx.method |
Method |
HTTP method |
ctx.headers |
Vec<(String, String)> |
Request headers |
ctx.body |
Vec<u8> |
Raw request body |
ctx.url |
String |
Full request URL |
ctx.wildcard |
Option<String> |
Catch-all wildcard tail |
Convenience methods: ctx.header("name"), 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.
Routing Conventions
| Pattern | Route | Description |
|---|---|---|
index.rs |
/ |
Index handler for the enclosing directory |
about.rs |
/about |
Named route |
og.png.rs |
/og.png |
Dotted filename — serves at the literal path including the extension |
_postId/index.rs |
/:postId |
Dynamic segment — generates a typed Params struct |
all.rs |
/* |
Catch-all wildcard — remaining path in ctx.wildcard |
middleware.rs |
— | Wraps all handlers in this directory and below |
not_found.rs |
— | Custom 404 handler |
Dynamic parameters
Prefix a directory with _ to capture a path segment. The build script generates a Params struct automatically:
// src/routes/posts/_postId/index.rs
use Params; // generated: pub struct Params { pub post_id: String }
Dotted filenames
Name a source file og.png.rs and the handler serves at the URL path og.png — the .rs extension is stripped but all other dots are preserved. A request to /app/42/og.png hits the handler in src/routes/app/_id/og.png.rs. This is useful for dynamically generated assets like images or feeds that need a specific file extension in the URL:
// src/routes/app/_id/og.png.rs → serves at /app/:id/og.png
Under the hood, the build script converts dots to underscores for the Rust module name (og.png.rs → mod og_png) and emits a #[path = "og.png.rs"] attribute so the compiler can find the source file.
Typed search params
Define a SearchParams struct in a route file and the query string is deserialized into ctx.search:
Untyped query params are always available via ctx.query.
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 this signature:
// src/routes/middleware.rs
use ;
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 → 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
// src/routes/middleware.rs
use ;
See the json-api example for a working CORS middleware.
Catch-all wildcards
Name a file all.rs to capture the entire remaining path. The matched tail is available via ctx.wildcard:
// src/routes/files/all.rs
A request to /files/docs/intro.md matches the wildcard and ctx.wildcard contains Some("docs/intro.md"). See examples/custom-404 for a working example.
Custom 404 handler
Place a not_found.rs file at the routes root (or in a subdirectory) to handle requests that don't match any route. The handler has the same signature as a regular route handler:
// src/routes/not_found.rs
use ;
use Cow;
Without a custom not_found.rs, the library returns a plain-text 404 response. All 404 responses are certified under a single canonical path to prevent memory growth from bot scans. See examples/custom-404 for a working example.
Route attribute override
Use #[route(path = "...")] to override the filename-derived segment. Useful for serving content at reserved names like /middleware:
// src/routes/mw_page.rs
Certification Modes
Every HTTP response served by an IC canister can be cryptographically certified so boundary nodes can verify it was not tampered with. This library supports three certification modes, configurable per-route via the #[route] attribute:
Choosing a mode
| Mode | When to use | Example routes |
|---|---|---|
| Response-only (default) | 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 |
Start with the default (response-only). It requires no configuration and is correct for 90% of routes.
Response-only (default — no attribute needed)
// Just write your handler — ResponseOnly is automatic
Skip certification
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
See the api-authentication example for a complete demonstration of both patterns.
Authenticated (full certification preset)
Custom full certification
Setup with static assets
Configure the asset router and certify static assets in a single builder chain during init/post_upgrade:
use ;
static ASSET_DIR: Dir = include_dir!;
For different certification modes per directory:
use CertificationMode;
See the certification-modes and api-authentication examples for complete, deployable demonstrations.
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.
Configuration
Security headers
setup
.with_config
.build;
Individual fields can be overridden on any preset. See SecurityHeaders for all available fields.
Cache control & invalidation
use HashMap;
use Duration;
use ;
setup
.with_config
.build;
Programmatic invalidation:
invalidate_path— single pathinvalidate_prefix— all paths under a prefixinvalidate_all_dynamic— all dynamic assets
Examples
Each example is a complete, deployable ICP canister. Clone the repo and dfx deploy from any example directory.
| Example | Description |
|---|---|
askama-basic |
Compile-time HTML templates with Askama |
tera-basic |
Runtime HTML templates with Tera |
htmx-app |
Server-rendered blog with HTMX partial updates and static assets |
json-api |
RESTful JSON API with CRUD, method routing, and CORS middleware |
security-headers |
Security header presets: strict, permissive, and custom |
cache-invalidation |
TTL-based cache expiry and explicit invalidation |
custom-404 |
Styled 404 page via not_found.rs |
certification-modes |
Skip, response-only, authenticated, and custom certification modes |
api-authentication |
Why authenticated endpoints need full certification |
react-app |
React SPA with TanStack Router/Query, per-route SEO meta tags, and canister API calls |
How This Library Was Built
[!NOTE] This project was built using the RALPH loop technique: detailed specs for every feature, an implementation plan divided into phases, and a
loop.shscript that feeds each phase to an AI builder agent one session at a time — keeping the context window focused for maximum output quality. Read more in RALPH.md or browse the full specs.
Updates
See the CHANGELOG for details on updates.
Author
- Twitter: @kristoferlund
- Discord: kristoferkristofer
- Telegram: @kristoferkristofer
Contributing
Contributions are welcome. Please submit your pull requests or open issues to propose changes or report bugs.
License
This project is licensed under the MIT License. See the LICENSE file for more details.