Cannoli Web Framework

Build web applications with Strada's built-in web framework.

📦 Download Cannoli on GitHub

Overview

Cannoli is Strada's built-in web framework for building HTTP servers. It provides:

Routing

URL path matching and HTTP method handling

Request Parsing

Query params, form data, JSON bodies, headers, cookies

Response Building

JSON, HTML, redirects, custom headers, cookies

File Uploads

Multipart form-data parsing for file uploads

SSL/TLS

HTTPS support with certificate configuration

Static Files

Built-in static file serving with directory listing

Quick Start

func handle(scalar $c) hash {
    my str $path = $c->path();

    if ($path eq "/") {
        return $c->json({
            "message" => "Hello, Cannoli!"
        });
    }

    if ($path eq "/greet") {
        my str $name = $c->param("name");
        if ($name eq "") {
            $name = "World";
        }
        return $c->text("Hello, " . $name . "!");
    }

    return $c->not_found();
}

func main() int {
    Cannoli::run(8080, \&handle);
    return 0;
}

Run with:

./strada -r myapp.strada

Visit http://localhost:8080/ to see your app.

The Cannoli Context Object

Every request handler receives a Cannoli context object ($c) with methods for accessing request data and building responses.

Request Methods

MethodDescription
$c->path()Get request path (e.g., "/users")
$c->method()Get HTTP method (GET, POST, etc.)
$c->param($name)Get query/form parameter
$c->params()Get all parameters as hash
$c->body()Get raw request body
$c->json_body()Parse body as JSON
$c->header($name)Get request header
$c->headers()Get all headers as hash
$c->cookie($name)Get cookie value
$c->cookies()Get all cookies as hash
$c->user_agent()Get User-Agent header
$c->host()Get Host header
$c->remote_addr()Get client IP address
$c->is_ajax()Check for XMLHttpRequest
$c->accepts_json()Check Accept header for JSON

Response Methods

MethodDescription
$c->status($code)Set HTTP status code
$c->content_type($type)Set Content-Type header
$c->set_header($name, $val)Set response header
$c->set_cookie($name, $val, $opts)Set cookie
$c->write_body($data)Write to response body
$c->build_response()Build final response hash

Response Shortcuts

MethodDescription
$c->json($data)Return JSON response (200)
$c->text($str)Return plain text response
$c->html($str)Return HTML response
$c->redirect($url)Redirect (302)
$c->redirect_permanent($url)Redirect (301)
$c->not_found()Return 404 response
$c->error($code, $msg)Return error response

Routing Examples

func handle(scalar $c) hash {
    my str $path = $c->path();
    my str $method = $c->method();

    # Static routes
    if ($path eq "/") {
        return $c->html("<h1>Welcome</h1>");
    }

    # Method-based routing
    if ($path eq "/users") {
        if ($method eq "GET") {
            return list_users($c);
        }
        if ($method eq "POST") {
            return create_user($c);
        }
    }

    # Path matching with regex
    if ($path =~ /^\/users\/([0-9]+)$/) {
        my str $id = $1;
        return get_user($c, $id);
    }

    # Path prefix matching
    if ($path =~ /^\/api\//) {
        return handle_api($c);
    }

    return $c->not_found();
}

Request Data

Query Parameters

# GET /search?q=hello&limit=10

func handle_search(scalar $c) hash {
    my str $query = $c->param("q");
    my str $limit = $c->param("limit");

    if ($limit eq "") {
        $limit = "20";  # Default value
    }

    return $c->json({
        "query" => $query,
        "limit" => int($limit)
    });
}

JSON Body

# POST /api/users with JSON body

func create_user(scalar $c) hash {
    my hash %data = $c->json_body();

    my str $name = $data{"name"};
    my str $email = $data{"email"};

    if ($name eq "" || $email eq "") {
        return $c->error(400, "Name and email required");
    }

    # Save user...

    $c->status(201);
    return $c->json({
        "id" => 123,
        "name" => $name,
        "email" => $email
    });
}

Form Data

# POST /login with form data

func handle_login(scalar $c) hash {
    my str $username = $c->param("username");
    my str $password = $c->param("password");

    if (authenticate($username, $password)) {
        $c->set_cookie("session", "abc123", "Path=/; HttpOnly");
        return $c->redirect("/dashboard");
    }

    return $c->error(401, "Invalid credentials");
}

File Uploads

func handle_upload(scalar $c) hash {
    if ($c->has_file("avatar") == 0) {
        return $c->error(400, "No file uploaded");
    }

    my str $filename = $c->file_name("avatar");
    my int $size = $c->file_size("avatar");
    my str $content_type = $c->file_type("avatar");
    my str $content = $c->file_content("avatar");

    # Validate file type
    if ($content_type !~ /^image\//) {
        return $c->error(400, "Only images allowed");
    }

    # Save file
    spew("/uploads/" . $filename, $content);

    return $c->json({
        "filename" => $filename,
        "size" => $size,
        "type" => $content_type
    });
}

Test with curl:

curl -F "avatar=@photo.jpg" http://localhost:8080/upload

Headers and Cookies

Request Headers

func handle(scalar $c) hash {
    my str $auth = $c->header("Authorization");
    my str $ua = $c->user_agent();

    if ($auth eq "") {
        return $c->error(401, "Authorization required");
    }

    # Validate token...
    return $c->json({"ok" => 1});
}

Response Headers

func handle(scalar $c) hash {
    # Set custom headers
    $c->set_header("X-Custom-Header", "value");

    # CORS headers
    $c->set_header("Access-Control-Allow-Origin", "*");

    # Cache control
    $c->set_header("Cache-Control", "max-age=3600");

    return $c->json({"data" => "here"});
}

Cookies

# Reading cookies
my str $session = $c->cookie("session");

# Setting cookies
$c->set_cookie("session", "abc123", "Path=/; HttpOnly; Secure");

# Delete cookie (set expiry in past)
$c->set_cookie("session", "", "Path=/; Max-Age=0");

Chunked Responses (Streaming)

func handle_stream(scalar $c) hash {
    $c->status(200);
    $c->content_type("text/plain");
    $c->start_chunked();  # Sends headers

    # Stream data in chunks
    for (my int $i = 0; $i < 5; $i++) {
        $c->write_chunk("Chunk " . $i . "\n");
        sys::sleep(1);
    }

    $c->end_chunked();  # Sends final 0\r\n\r\n
    return $c->build_response();
}

HTTPS/SSL

Cannoli supports HTTPS with certificate configuration:

# Run with SSL
./cannoli --ssl --cert cert.pem --key key.pem --port 8443

Or programmatically:

func main() int {
    my hash %opts = {
        "port" => 8443,
        "ssl" => 1,
        "cert" => "/path/to/cert.pem",
        "key" => "/path/to/key.pem"
    };
    Cannoli::run_with_opts(%opts, \&handle);
    return 0;
}

Static File Server

Serve static files with directory listing:

# Command line
./cannoli --static /var/www/html --listing --port 8080

# Shorthand (positional argument)
./cannoli /var/www/html --listing

Complete Example

# A simple JSON API

my array @todos = [];
my int $next_id = 1;

func handle(scalar $c) hash {
    my str $path = $c->path();
    my str $method = $c->method();

    # CORS for all requests
    $c->set_header("Access-Control-Allow-Origin", "*");

    # List all todos
    if ($path eq "/todos" && $method eq "GET") {
        return $c->json({ "todos" => @todos });
    }

    # Create todo
    if ($path eq "/todos" && $method eq "POST") {
        my hash %body = $c->json_body();
        my hash %todo = {
            "id" => $next_id,
            "title" => $body{"title"},
            "done" => 0
        };
        $next_id++;
        push(@todos, %todo);
        $c->status(201);
        return $c->json(%todo);
    }

    return $c->not_found();
}

func main() int {
    say("Starting server on http://localhost:8080");
    Cannoli::run(8080, \&handle);
    return 0;
}