Skip to content

Web Framework

Incan includes a web framework that currently lowers to Axum, giving you Flask/FastAPI-like syntax with a native async server backend.

Quick Start

Contributor source-build fallback

If you are working from a compiler checkout instead of a toolchain install, you can run the repository-built binary directly:

  • from the repository root:
./target/release/incan ...
  • or via an absolute path (from anywhere):
/absolute/path/to/incan run path/to/file.incn
from std.web import App, route, Response, Json
from std.serde import json
import std.async

@derive(json)
model Greeting:
    message: str

@route("/")
async def index() -> Response:
    return Response.html("<h1>Hello from Incan!</h1>")

@route("/api/greet/{name}")
async def greet(name: str) -> Json[Greeting]:
    return Json(Greeting(message=f"Hello, {name}!"))

def main() -> None:
    app = App()
    app.run(port=8080)

Build and run:

incan build examples/web/hello_web.incn
./target/incan/hello_web/target/release/hello_web

Note: the first build may download Rust crates via Cargo (can take minutes) and requires internet access.

Routes

Define routes using the @route decorator:

from std.web import route, Response, GET, POST
import std.async

@route("/path")
async def handler() -> Response:
    ...

@route("/api/resource", methods=[GET])
async def get_resource() -> Response:
    ...

@route("/api/resource", methods=[POST])
async def create_resource() -> Response:
    ...

Path Parameters

Use {name} syntax for path parameters:

from std.web import route, Json
import std.async

@route("/users/{id}")
async def get_user(id: int) -> Json[User]:
    user = find_user(id)?
    return Json(user)

@route("/posts/{year}/{month}")
async def get_posts(year: int, month: int) -> Json[list[Post]]:
    return Json(fetch_posts(year, month))

Use a scalar parameter when the handler body needs the captured value. A route may instead declare an unused typed Path[T] extractor with the wildcard pattern when only Axum's extraction and rejection behavior is required:

from std.web import route, Json, Path
import std.async

@route("/health/{probe}")
async def health_probe(_: Path[str]) -> Json[Health]:
    return Json(Health(ok=True))

HTTP Methods

Specify allowed methods with the methods parameter. Handlers can be registered for multiple HTTP methods by passing multiple entries. Import the method constants from the web prelude (e.g. GET, POST). Supported methods are GET, POST, PUT, DELETE, and PATCH.

from std.web import route, Json, Response, GET, POST, PUT, DELETE
import std.async

@route("/items/ping", methods=[GET, POST])
async def ping_items() -> Response:
    return Response.ok()

@route("/items", methods=[GET])
async def list_items() -> Json[list[Item]]:
    ...

@route("/items", methods=[POST])
async def create_item(body: Json[CreateItem]) -> Json[Item]:
    ...

@route("/items/{id}", methods=[PUT])
async def update_item(id: int) -> Response:
    ...

@route("/items/{id}", methods=[DELETE])
async def delete_item(id: int) -> Response:
    ...

Responses

JSON Responses

Use Json[T] for JSON responses. The inner type must have @derive(json):

from std.web import route, Json
from std.serde import json
import std.async

@derive(json)
model User:
    id: int
    name: str
    email: str

@route("/api/user/{id}")
async def get_user(id: int) -> Json[User]:
    user = User(id=id, name="Alice", email="alice@example.com")
    return Json(user)

HTML Responses

Return HTML with Response.html():

from std.web import route, Response
import std.async

@route("/")
async def index() -> Response:
    return Response.html("<h1>Welcome!</h1>")

Status Codes

Use Response methods for different status codes:

from std.web import route, Response
import std.async

@route("/health")
async def health() -> Response:
    return Response.ok()  # 200

@route("/created")
async def created() -> Response:
    return Response.created()  # 201

@route("/empty")
async def empty() -> Response:
    return Response.no_content()  # 204

@route("/error")
async def error() -> Response:
    return Response.bad_request("Invalid input")  # 400

@route("/missing")
async def missing() -> Response:
    return Response.not_found("Resource not found")  # 404

@route("/server-error")
async def server_error() -> Response:
    return Response.internal_error("Something went wrong")  # 500

Request Data

Extracting Path Parameters

Path parameters are automatically extracted into function arguments:

from std.web import route, Json
import std.async

@route("/users/{user_id}/posts/{post_id}")
async def get_post(user_id: int, post_id: int) -> Json[Post]:
    ...

Query Parameters

Use Query[T] for query string parameters:

from std.web import route, Json, Query
from std.serde import json
import std.async

@derive(json)
model SearchParams:
    q: str
    limit: int = 10

@route("/search")
async def search(params: Query[SearchParams]) -> Json[list[Result]]:
    results = do_search(params.q, params.limit)
    return Json(results)

JSON Body

Use Json[T] as a parameter for JSON request bodies:

from std.web import route, Json, POST
from std.serde import json
import std.async

@derive(json)
model CreateUser:
    name: str
    email: str

@route("/users", methods=[POST])
async def create_user(body: Json[CreateUser]) -> Json[User]:
    user = User(id=1, name=body.name, email=body.email)
    return Json(user)

Application

Starting the Server

Create an App and call run():

from std.web import App

def main() -> None:
    app = App()
    app.run(host="0.0.0.0", port=3000)

Parameters:

  • host: Bind address (default: "127.0.0.1")
  • port: Port number (default: 8080)

How It Works

When you compile an Incan web application:

  1. Routes are collected from @route decorators
  2. Handlers become async Rust functions with Axum extractors
  3. Models with @derive(json) get serde derives
  4. app.run() becomes Axum router setup + tokio server

The generated Rust code uses:

  • axum::Router for routing
  • axum::Json for JSON request/response
  • axum::extract::Path for path parameters
  • axum::extract::Query for query parameters
  • tokio for async runtime

Complete Example

"""
A simple REST API for managing items.
"""

from std.web import App, route, Response, Json, GET, POST, DELETE
from std.serde import json
import std.async


@derive(json)
model Item:
    id: int
    name: str
    price: float


# In-memory storage (in a real app, use a database)
items: list[Item] = []


@route("/api/items", methods=[GET])
async def list_items() -> Json[list[Item]]:
    """List all items."""
    return Json(items)


@route("/api/items", methods=[POST])
async def create_item(body: Json[Item]) -> Json[Item]:
    """Create a new item."""
    item = Item(id=body.id, name=body.name, price=body.price)
    items.append(item)
    return Json(item)


@route("/api/items/{id}", methods=[GET])
async def get_item(id: int) -> Response:
    """Get an item by ID."""
    for item in items:
        if item.id == id:
            return Json(item)
    return Response.not_found("Item not found")


@route("/api/items/{id}", methods=[DELETE])
async def delete_item(id: int) -> Response:
    """Delete an item by ID."""
    for i, item in enumerate(items):
        if item.id == id:
            items.pop(i)
            return Response.no_content()
    return Response.not_found("Item not found")


def main() -> None:
    println("Starting API server at http://localhost:8080")
    app = App()
    app.run(port=8080)

Performance

With the current Rust/Axum-backed path, your web application runs with:

  • Native server path — no Python interpreter process in the request path
  • Tokio-backed async — an efficient async runtime without Python event-loop compatibility claims
  • No garbage collector — predictable latency
  • Low memory usage — Rust's ownership model

That makes Incan a reasonable fit for API and microservice code where Python-readable source and a native async server backend both matter.

See Also