Skip to content

Routing

Lee Yunjin edited this page Oct 7, 2026 · 1 revision

Routing

Headers: <cwist/app.h> (app routes), <cwist/net/http/mux.h> (standalone router), <cwist/sys/app/endpoint_opts.h> (endpoint flags)

App routes

typedef void (*cwist_handler_func)(cwist_http_request *req, cwist_http_response *res);

void cwist_app_get   (cwist_app *app, const char *path, cwist_handler_func handler);
void cwist_app_post  (cwist_app *app, const char *path, cwist_handler_func handler);
void cwist_app_put   (cwist_app *app, const char *path, cwist_handler_func handler);
void cwist_app_delete(cwist_app *app, const char *path, cwist_handler_func handler);
void cwist_app_patch (cwist_app *app, const char *path, cwist_handler_func handler);

Registering the same method and path again replaces the earlier handler.

Path parameters

A segment that starts with : captures one path segment into req->path_params:

static void get_comment(cwist_http_request *req, cwist_http_response *res) {
    const char *post = cwist_query_map_get(req->path_params, "post");
    const char *id   = cwist_query_map_get(req->path_params, "id");
    ...
}

cwist_app_get(app, "/posts/:post/comments/:id", get_comment);

Matching rules

These follow from the implementation and the behavior measured with cwist_app_dispatch_memory():

  • Routes without parameters are stored in a hash table (O(1) lookup). Parameterized routes are tried in order after an exact miss.
  • The query string is not part of matching: /users/42?q=1 matches /users/:id, and the query is in req->query_params.
  • A trailing slash is significant: /x/ does not match /x.
  • A parameter matches exactly one segment: /users/42/extra does not match /users/:id.
  • The method must match. A request for a registered path with another method, including HEAD for a GET route, gets 404 Not Found (no 405 and no automatic HEAD). Register HEAD handling explicitly if you need it. Static files and assets do answer HEAD.
  • On a miss, the dispatcher tries content-hashed assets, then static directories, then the error handler for 404 (see Error Handling).

Routes with a context (_ex)

typedef void (*cwist_handler_ex_func)(void *user_ctx, cwist_http_request *req,
                                      cwist_http_response *res);
typedef void (*cwist_handler_ctx_destroy_func)(void *user_ctx);

cwist_error_t cwist_app_get_ex   (cwist_app *app, const char *path, cwist_handler_ex_func handler,
                                  void *user_ctx, cwist_handler_ctx_destroy_func destroy);
/* also cwist_app_post_ex, _put_ex, _delete_ex, _patch_ex */

The handler receives the pointer it was registered with, so per-route state needs no globals. Language bindings use these to register closures.

Ownership of user_ctx passes to the app on every call. When destroy is not NULL it runs exactly once with user_ctx:

  • when the app is destroyed;
  • when the same method and path (without :param segments) is registered again by any routing function, unless the new registration passes the same non-NULL user_ctx;
  • immediately, when registration fails. The function then returns INT16 -1; success is INT16 0.

Multiport sub-apps share the context without owning it. Pass destroy = NULL to keep ownership with the caller. The handler can run on several threads at once, so shared context needs its own locking.

typedef struct { const char *greeting; } greeter;

static void greet(void *ctx, cwist_http_request *req, cwist_http_response *res) {
    const greeter *g = ctx;
    (void)req;
    cwist_sstring_assign(res->body, g->greeting);
}

greeter *g = cwist_alloc(sizeof(*g));     /* <cwist/core/mem/alloc.h> */
g->greeting = "hello";
cwist_app_get_ex(app, "/greet", greet, g, cwist_free);

Endpoint flags (_opt)

typedef uint32_t cwist_endpoint_opt_t;

#define CWIST_DYNAMIC        (1u << 0)  /* default */
#define CWIST_ENDPOINT_FIXED (1u << 1)  /* request-invariant hint */
#define CWIST_ENDPOINT_FILE  (1u << 2)  /* large-file zero-copy path */

void cwist_app_get_opt(cwist_app *app, const char *path, cwist_handler_func handler,
                       cwist_endpoint_opt_t opts);
/* also _post_opt, _put_opt, _delete_opt, _patch_opt, _ws_opt */
  • CWIST_ENDPOINT_FILE lets cwist_http_response_send_file() stream files larger than CWIST_HTTP_MAX_BODY_SIZE with sendfile(2) on Linux, macOS and FreeBSD.
  • CWIST_ENDPOINT_FIXED does not enable a response cache on dev; every request reaches the handler. The v3.9 release adds a separate CWIST_ENDPOINT_PUBLIC_FIXED flag. See FIXED Endpoint Cache.

Flags combine with |. cwist_endpoint_has(opts, flag) tests a flag (cwist_endpoint_has_extern() is the out-of-line version for bindings). Inside a handler, req->endpoint_opts holds the active route's flags.

Named routes and URL building

cwist_app_get_named(app, "/posts/:id", "post", show_post);
/* also _post_named, _put_named, _delete_named, _patch_named */

cwist_query_map *p = cwist_query_map_create();
cwist_query_map_set(p, "id", "7");
char *url = cwist_url_for(app, "post", p);   /* free with cwist_free() */
cwist_query_map_destroy(p);

cwist_url_for() returns NULL for an unknown name and copies the path unchanged when it has no parameters or params is NULL. A :param with no value in params is left as :param.

Known issue: when it substitutes parameters, the current implementation drops the leading / (/posts/:id becomes posts/7, while /about stays /about). Prepend the slash yourself until this is fixed.

WebSocket routes

void cwist_app_ws(cwist_app *app, const char *path, cwist_ws_handler_func handler);
void cwist_app_ws_async(cwist_app *app, const char *path,
                        cwist_ws_on_message_t on_message, void *user_data);

See WebSocket.

Static directories

cwist_app_static(app, "/static", "./public");
cwist_app_static_with_cache(app, "/assets", "./dist", "public, max-age=31536000, immutable");

See Static Files and Assets.

The standalone mux router

Header: <cwist/net/http/mux.h>

cwist_mux_router is a router you can use on its own, outside cwist_app (for example inside a custom server loop). It supports exact routes (hashed), :param routes, routes that end in *, route groups with a path prefix, and per-route middleware.

cwist_mux_router *r = cwist_mux_router_create();
cwist_mux_handle(r, CWIST_HTTP_GET, "/health", health);
cwist_mux_handle(r, CWIST_HTTP_GET, "/files/*", files);

cwist_mux_group *api = cwist_mux_group_create(r, "/api/v1");
cwist_mux_group_handle(api, CWIST_HTTP_GET, "/users/:id", get_user);   /* /api/v1/users/:id */

cwist_mux_route *route = cwist_mux_find_route(r, CWIST_HTTP_GET, "/health");
cwist_mux_route_use(route, my_middleware);

if (!cwist_mux_serve(r, req, res)) {
    res->status_code = CWIST_HTTP_NOT_FOUND;
}

cwist_mux_group_destroy(api);
cwist_mux_router_destroy(r);
Function Description
cwist_mux_router *cwist_mux_router_create(void) New router.
void cwist_mux_router_destroy(cwist_mux_router *router) Free the router and its routes.
void cwist_mux_handle(cwist_mux_router *router, cwist_http_method_t method, const char *path, cwist_http_handler_func handler) Register a route.
cwist_mux_route *cwist_mux_find_route(cwist_mux_router *router, cwist_http_method_t method, const char *path) Look a route up.
cwist_mux_group *cwist_mux_group_create(cwist_mux_router *router, const char *prefix) Route group with a path prefix.
void cwist_mux_group_handle(cwist_mux_group *group, cwist_http_method_t method, const char *path, cwist_http_handler_func handler) Register inside a group.
void cwist_mux_group_destroy(cwist_mux_group *group) Free the group (routes stay in the router).
void cwist_mux_route_use(cwist_mux_route *route, cwist_middleware_func mw) Attach middleware to one route.
bool cwist_mux_serve(cwist_mux_router *router, cwist_http_request *req, cwist_http_response *res) Dispatch; false when no route matched.

Design notes and references for the router's matching algorithm: Mux Algorithm References.

Clone this wiki locally