Skip to content

Composable subrouters and routers #34

@oduortoni

Description

@oduortoni

Router Specification

Overview

This document specifies the design of a composable, Express-style router for the C HTTP Server. The router enables modular route organization through nested sub-routers that can be mounted at different path prefixes.

Design Principles

  1. Lightweight - Uses simple structs and function pointers, no complex abstractions
  2. Composable - Routers can contain other routers, enabling hierarchical organization
  3. Modular - Each router can be defined in its own file and composed later

Core Concepts

Router

A router is a flat collection of routes (pattern + handler pairs). Sub-routers are flattened at mount time for zero runtime overhead.

typedef struct Router Router;

struct Router {
    char* patterns[MAX_ROUTES];           // Route patterns (regex)
    regex_t compiled_patterns[MAX_ROUTES]; // Compiled regex patterns
    HandlerFunc handlers[MAX_ROUTES];     // Handler functions
    int route_count;                      // Number of routes
};

Route

A route is a mapping from a URL pattern to a handler function.

typedef int (*HandlerFunc)(ResponseWriter* w, Request* r);

Mounting

Mounting is the process of flattening a sub-router into a parent router at a specific path prefix. All routes in the sub-router are prefixed and copied into the parent at mount time. The child router is freed after mounting.

Key insight: Since C servers configure routes at startup and never change them at runtime, we flatten the route tree at mount time for zero runtime overhead. Request matching becomes a simple O(n) loop with no recursion or pointer chasing.

API Design

Creating Routers

// Create a new router
Router* router_create(void);

// Free a router and all its sub-routers
void router_free(Router* router);

Adding Routes

// Add a route to a router
// pattern: regex pattern (e.g., "^/$", "^/users/(.*)$")
// handler: function to handle matching requests
void router_add(Router* router, const char* pattern, HandlerFunc handler);

Mounting Sub-Routers

// Mount a sub-router at a prefix (flattens immediately)
// parent: the parent router
// prefix: path prefix (e.g., "/api", "/blog")
// child: the sub-router to mount (will be freed after flattening)
void router_mount(Router* parent, const char* prefix, Router* child);

Important: The child router is freed after mounting since all its routes are copied into the parent. Do not use the child router after mounting.

Route Matching

// Find a handler for a given path
// Returns the handler function or NULL if no match
HandlerFunc router_match(Router* router, const char* path, Request* req);

Usage Examples

Basic Routing (Flat)

// Create a simple router
Router* main_router = router_create();

// Add routes
router_add(main_router, "^/$", Index);
router_add(main_router, "^/about$", About);
router_add(main_router, "^/404$", Error404);

// Use it
http.ListenAndServe(hostname, main_router);

Nested Routing (Composable)

// Create a blog router
Router* blog_router = router_create();
router_add(blog_router, "^/$", BlogIndex);           // /blog/
router_add(blog_router, "^/post/(.*)$", BlogPost);   // /blog/post/:id
router_add(blog_router, "^/archive$", BlogArchive);  // /blog/archive

// Create an API router
Router* api_router = router_create();
router_add(api_router, "^/users$", GetUsers);        // /api/users
router_add(api_router, "^/posts$", GetPosts);        // /api/posts

// Create main router and mount sub-routers
Router* main_router = router_create();
router_add(main_router, "^/$", Index);
router_add(main_router, "^/about$", About);

router_mount(main_router, "/blog", blog_router);     // with the /blog prefix
router_mount(main_router, "/api", api_router);       // with the /api prefix

// Final route structure:
// /              -> Index
// /about         -> About
// /blog/         -> BlogIndex
// /blog/post/123 -> BlogPost
// /blog/archive  -> BlogArchive
// /api/users     -> GetUsers
// /api/posts     -> GetPosts

Modular Organization

// src/app/blog/routes.c
Router* create_blog_routes(void) {
    Router* r = router_create();
    router_add(r, "^/$", BlogIndex);
    router_add(r, "^/post/(.*)$", BlogPost);
    router_add(r, "^/archive$", BlogArchive);
    router_add(r, "^/tags$", BlogTags);
    return r;
}

// src/app/api/routes.c
Router* create_api_routes(void) {
    Router* r = router_create();
    router_add(r, "^/users$", GetUsers);
    router_add(r, "^/posts$", GetPosts);
    router_add(r, "^/comments$", GetComments);
    return r;
}

// src/app/admin/routes.c
Router* create_admin_routes(void) {
    Router* r = router_create();
    router_add(r, "^/$", AdminDashboard);
    router_add(r, "^/users$", ManageUsers);
    router_add(r, "^/settings$", Settings);
    return r;
}

// src/main.c
int main() {
    Router* main = router_create();
    router_add(main, "^/$", Index);
    router_add(main, "^/about$", About);
    router_add(main, "^/404$", Error404);
    
    // Mount sub-routers
    router_mount(main, "/blog", create_blog_routes());
    router_mount(main, "/api", create_api_routes());
    router_mount(main, "/admin", create_admin_routes());
    
    http.ListenAndServe(hostname, main);
    return 0;
}

Route Matching Algorithm

Since routes are flattened at mount time, matching is a simple linear search:

  1. Iterate through routes - Loop through the flat array of patterns
  2. Test each pattern - Use regex to match against the request path
  3. Return first match - Return the handler for the first matching pattern
  4. Return NULL if no match - No route matched the request

Performance: O(n) where n is the total number of routes. No recursion, no pointer chasing, cache-friendly sequential access.

NOTE

When mounting a sub-router, patterns are prefixed and flattened into the parent:

Conclusion

This router design provides a clean, composable way to organize routes in a C HTTP server while maintaining maximum performance. It is a model of nested routers during setup, but flattens everything at mount time for zero runtime overhead.

Key insight: C servers configure routes at startup and never change them. By flattening the route tree at mount time, we get the best of both worlds:

  • Development time: Composable, modular, reusable routers
  • Runtime: Simple O(n) loop, no recursion, cache-friendly

The implementation is straightforward, requiring only basic struct manipulation, string operations, and regex compilation. No complex algorithms, no hidden costs, just fast, predictable route matching.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions