1
0
Fork 0
Really cool web framework.
Find a file
2026-01-07 22:37:12 -06:00
docs Documentation, database API simplification 2026-01-07 22:37:12 -06:00
src Implement HTTP layer, routing layer, database layer, static file server 2026-01-05 20:12:17 -06:00
test Documentation, database API simplification 2026-01-07 22:37:12 -06:00
web Documentation, database API simplification 2026-01-07 22:37:12 -06:00
.gitignore Implement HTTP layer, routing layer, database layer, static file server 2026-01-05 20:12:17 -06:00
CLAUDE.md Documentation, database API simplification 2026-01-07 22:37:12 -06:00
coding_style.md Implement HTTP layer, routing layer, database layer, static file server 2026-01-05 20:12:17 -06:00
lobster.md Add LObster support, easier JSON building 2026-01-07 15:07:00 -06:00
Makofile Lots of delicious helpers 2026-01-07 14:03:17 -06:00
plan.md Implement HTTP layer, routing layer, database layer, static file server 2026-01-05 20:12:17 -06:00
README.md Documentation, database API simplification 2026-01-07 22:37:12 -06:00
web.hpp Lots of improvements, more integrations, etc 2026-01-06 13:59:07 -06:00

C++ Web Framework

A modern, batteries-included web framework for C++17 that makes building web applications actually enjoyable. No massive dependencies, no complexity for complexity's sake—just a clean, powerful toolkit for building real web apps.

What is this?

This is a complete web framework written from scratch in C++17. It includes everything you need to build a production web application: an HTTP server, a fast router, database abstraction, a template engine, form validation, sessions, authentication, and more. Think of it as bringing the convenience of frameworks like Laravel or Flask to C++, without sacrificing performance or control.

Quick Example

Here's a complete working web app:

#include "web.hpp"

int main() {
    // Set up your database
    web::db::setDefault(web::db::sqlite("app.db"));

    // Create your router
    web::routing::Router router;

    // Define your routes
    router.get("/", [](const auto& req, const auto& params) {
        return web::http::HttpResponse()
            .html("<h1>Hello, World!</h1>");
    });

    router.get("/users/:id", [](const auto& req, const auto& params) {
        auto user = web::db::db().table("users")
            .where("id", "=", params[0])
            .first();

        return web::http::HttpResponse()
            .json(user->toJson());
    });

    // Start the server
    web::http::HttpServer server(8080, router);
    server.run();
}

That's it. No configuration files, no build system wrestling, no dependency hell. Just C++ code.

The HTTP Server

The framework includes a full-featured HTTP server built from scratch using POSIX sockets. It handles all the low-level networking, request parsing, and response formatting so you don't have to think about it. The server is multi-threaded, handles keep-alive connections, and supports all standard HTTP methods.

You can serve thousands of requests per second on modest hardware. The server handles chunked encoding, gzip compression, cookies, sessions—all the stuff you'd expect from a modern web server.

Routing That Makes Sense

The router uses a radix trie for blazing-fast path matching. You can define routes with dynamic segments, wildcards, and parameter extraction:

router.get("/users/:id", userHandler);
router.get("/posts/:category/:slug", postHandler);
router.get("/files/*", fileHandler);

Route parameters are automatically extracted and passed to your handlers. No manual parsing, no regex headaches.

Middleware

Middleware functions run before your route handlers, letting you do things like authentication, logging, or request transformation:

auto authMiddleware = [](auto& req, auto& resp) {
    if (!Auth::authenticated()) {
        resp.status(302).header("Location", "/login");
    }
};

router.get("/admin", adminHandler).middleware(authMiddleware);

Chain as many middleware as you need. They run in order, and any middleware can short-circuit the request.

Flexible Route Registration

You can register routes with single methods, multiple methods, or all methods:

router.get("/users", listUsers);
router.post("/users", createUser);
router.many({HttpMethod::GET, HttpMethod::POST}, "/contact", contactHandler);
router.any("/webhook", webhookHandler);
router.form("/login", loginHandler);  // GET + POST shorthand

The router automatically handles 404s for missing routes and 405s for valid routes with unsupported methods.

Database Layer

Work with SQLite or MySQL using a clean, fluent query builder that feels natural in C++:

// Connect to your database
db::setDefault(
    db::sqlite("database.db")
);

// Query with the builder
auto users = db::db().table("users")
    .where("status", "=", "active")
    .whereIn("role", {"admin", "moderator"})
    .orderBy("created_at", "DESC")
    .limit(10)
    .get();

// Insert data
auto userId = db::db().table("users").insert({
    {"name", "Alice"},
    {"email", "alice@example.com"},
    {"age", 30}
});

// Update records
db::db().table("users")
    .where("id", "=", userId)
    .update({{"last_login", "2025-01-07"}});

// Join tables
auto posts = db::db().table("posts")
    .join("users", "posts.user_id", "=", "users.id")
    .select({"posts.*", "users.name as author"})
    .get();

The query builder handles parameter binding automatically, protecting you from SQL injection. Values are strongly typed using a flexible db::Value type that handles integers, floats, strings, and nulls transparently.

Raw Queries

When you need raw SQL, you can use it with prepared statements:

auto result = db::db().query(
    "SELECT * FROM users WHERE id=? AND status=?",
    {5, "active"}
);

// Or with named parameters
auto result = db::db().query(
    "SELECT * FROM users WHERE id=:id AND status=:status",
    {{":id", 5}, {":status", "active"}}
);

Transactions

Handle transactions with simple begin/commit/rollback semantics:

try {
    db::db().beginTransaction();

    db::db().table("accounts").where("id", "=", 1).update({{"balance", 900}});
    db::db().table("accounts").where("id", "=", 2).update({{"balance", 1100}});

    db::db().commit();
} catch (const db::Exception& e) {
    db::db().rollback();
    std::cerr << "Transaction failed: " << e.what() << std::endl;
}

Template Engine

Write your views using a Blade-like template syntax that compiles to C++ at runtime. Templates are cached and incredibly fast:

{{-- templates/home.html --}}
@extends('layouts/main')

@section('title')
    Welcome, {{ user.name }}!
@endsection

@section('content')
    <h1>Hello, {{ user.name }}</h1>

    <p>You have {{ user.gold | numberFormat }} gold.</p>

    @if(user.level > 10)
        <span class="badge">Veteran Player</span>
    @endif

    <h2>Your Inventory</h2>
    @for(inventory as item)
        <div class="item">
            {{ item.name }} - Quantity: {{ item.quantity }}
        </div>
    @endfor

    @empty(quests)
        <p>No active quests.</p>
    @endempty
@endsection

The template engine supports:

Variables: Output data with {{ variable }} (auto-escaped) or {!! raw !!} (unescaped)

Filters: Transform values with pipes: {{ price | numberFormat }}, {{ name | upper }}, {{ date | prettyDate }}

Conditionals: Full if/elseif/else support plus @unless, @isset, @empty, @null helpers

Loops: Both @for and @while loops with key-value iteration support

Layouts: Extend parent templates with @extends, define sections with @section/@endsection, and insert them with @yield

Includes: Reuse partials with @include('partials/header')

Comments: Template comments with {{-- comment --}} that don't appear in output

Using Templates in Code

Render templates from your handlers:

TemplateEngine engine("./templates");

router.get("/profile", [&engine](auto& req, auto& params) {
    auto user = Auth::user();
    auto inventory = db::db().table("inventory")
        .where("user_id", "=", user["id"])
        .get();

    std::string html = engine.render("profile", {
        {"user", user},
        {"inventory", inventory}
    });

    return HttpResponse().html(html);
});

Templates are parsed once and cached. In development mode, the engine watches for file changes and automatically reloads. In production, disable auto-reload for maximum performance.

Form Validation

Validate user input with a declarative, Rails-inspired validation system:

auto result = validate(req.postData_, {
    {"username", "length:3-20|alphanum|unique:users,username"},
    {"email", "email|unique:users,email"},
    {"password", "length:8-100"},
    {"password_confirm", "confirm:password"},
    {"age", "int|min:13|max:120"},
    {"bio", "optional|length:0-500"}
});

if (!result.valid) {
    return HttpResponse().status(400).body(
        renderErrors(result.errors)
    );
}

// Use validated, type-converted data
std::string username = result.data["username"].asString();
int age = result.data["age"].asInt();

The validation system includes built-in rules for common scenarios:

String validation: length ranges, alpha/alphanumeric checks, email format, regex matching

Numeric validation: integer type checking, min/max values

Database validation: unique constraints, existence checks

Special rules: optional fields, default values, boolean parsing, field confirmation

Rules are composable—chain them with pipes to build complex validation logic. The validator handles type conversion automatically, so your validated data comes out in the right C++ types.

Custom Validators

Extend the system with your own validation rules:

class CustomRule : public ValidationRule {
public:
    bool validate(const std::string& value) override {
        // Your logic here
        return isValid;
    }

    std::string getErrorMessage() override {
        return "Custom validation failed";
    }
};

validator.registerRule("custom", std::make_unique<CustomRule>());

Sessions and Authentication

Built-in session management with strong security defaults:

// Initialize sessions
Session::initialize();

// Store data
Session::set("user_id", 42);
Session::set("preferences", {{"theme", "dark"}});

// Retrieve data
auto userId = Session::get("user_id").asInt();

// Check existence
if (Session::has("cart")) {
    auto cart = Session::get("cart");
}

Sessions use cryptographically random 64-character IDs, HTTPOnly and Secure cookie flags, SameSite protection, and automatic session regeneration. The session system tracks IP addresses and user agents to prevent session hijacking.

Authentication

The Auth helper makes user authentication trivial:

router.form("/login", [](auto& req, auto& params) {
    if (req.method_ == HttpMethod::POST) {
        if (Auth::login(req.postData_["username"], req.postData_["password"])) {
            return HttpResponse().redirect("/dashboard");
        }
        return HttpResponse().status(401).body("Invalid credentials");
    }

    return HttpResponse().html(renderLoginForm());
});

router.get("/logout", [](auto& req, auto& params) {
    Auth::logout();
    return HttpResponse().redirect("/");
});

// Check authentication in handlers
if (Auth::authenticated()) {
    auto user = Auth::user();
    std::cout << "Logged in as: " << user["username"].asString() << std::endl;
}

Passwords are hashed with Argon2, the current gold standard for password hashing. The Auth system integrates seamlessly with sessions and the database layer.

Static File Serving

Serve static assets with automatic MIME type detection, browser caching, and ETag support:

StaticFileHandler assets("./public");
assets.setCacheMaxAge(86400)      // Cache for 1 day
      .setIndexFile("index.html")  // Serve index.html for directories
      .enableETag(true);            // Enable ETag generation

router.get("/static/*", [&assets](auto& req, auto& params) {
    return assets.handle(req, req.path_);
});

The static file handler:

Detects MIME types automatically for HTML, CSS, JavaScript, images, fonts, videos, PDFs, and more

Handles caching with Cache-Control headers and configurable max-age

Generates ETags based on file size and modification time for efficient cache validation

Serves index files for directory requests

Prevents path traversal attacks with canonical path resolution

Returns proper status codes: 200 for success, 304 for not modified, 404 for not found, 403 for security violations

Request and Response

The HttpRequest object gives you access to everything about the incoming request:

router.get("/api/search", [](auto& req, auto& params) {
    // HTTP method
    HttpMethod method = req.method_;

    // URI components
    std::string path = req.path_;              // /api/search
    std::string query = req.queryString_;      // q=hello&page=2

    // Query parameters
    std::string searchQuery = req.queryParams_["q"];

    // POST data
    std::string username = req.postData_["username"];

    // Headers
    std::string userAgent = req.headers_["User-Agent"];

    // Cookies
    std::string session = req.cookies_["session"];

    // Raw body
    std::string rawBody = req.body_;
});

The HttpResponse uses a fluent builder pattern:

// Basic responses
return HttpResponse().status(200).body("OK");
return HttpResponse().html("<h1>Hello</h1>");
return HttpResponse().json("{\"status\": \"success\"}");
return HttpResponse().text("Plain text");

// With headers
return HttpResponse()
    .status(200)
    .header("Content-Type", "application/json")
    .header("X-Custom-Header", "value")
    .body(jsonData);

// Redirects
return HttpResponse().redirect("/new-location");
return HttpResponse().redirect("/moved", 301);  // Permanent

// File downloads
return HttpResponse().download(csvData, "export.csv", "text/csv");

// Images and assets
return HttpResponse().png(imageData);
return HttpResponse().jpeg(imageData);
return HttpResponse().css(stylesheetContent);
return HttpResponse().javascript(scriptContent);

// Cookies
return HttpResponse()
    .cookie("session", sessionId)
    .body("Logged in");

Building and Running

Requirements: C++17 compiler, SQLite3 development headers, optionally MySQL headers for MySQL support.

# Install dependencies (Ubuntu/Debian)
sudo apt-get install libsqlite3-dev libmysqlclient-dev

# Compile
g++ -std=c++17 -O3 -pthread \
    main.cpp \
    src/**/*.cpp \
    -lsqlite3 -lmysqlclient \
    -o webapp

# Run
./webapp

Or use the Mako build system:

# Makofile
name web
lang c++17
libs sqlite3 mysqlclient pthread
mako build
./build/web

A Complete Example

Here's a small blog application showing everything working together:

#include "web.hpp"

int main() {
    // Database setup
    db::setDefault(
        db::sqlite("blog.db")
    );

    db::db().exec(R"(
        CREATE TABLE IF NOT EXISTS posts (
            id INTEGER PRIMARY KEY,
            title TEXT,
            content TEXT,
            author TEXT,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    )");

    // Template engine
    TemplateEngine templates("./templates");

    // Router
    Router router;

    // Authentication middleware
    auto requireAuth = [](auto& req, auto& resp) {
        if (!Auth::authenticated()) {
            resp.redirect("/login");
        }
    };

    // List posts
    router.get("/", [&](auto& req, auto& params) {
        auto posts = db::db().table("posts")
            .orderBy("created_at", "DESC")
            .get();

        return HttpResponse().html(
            templates.render("home", {{"posts", posts}})
        );
    });

    // View single post
    router.get("/posts/:id", [&](auto& req, auto& params) {
        auto post = db::db().table("posts")
            .where("id", "=", params[0])
            .first();

        if (!post.has_value()) {
            return HttpResponse().status(404).body("Post not found");
        }

        return HttpResponse().html(
            templates.render("post", {{"post", *post}})
        );
    });

    // Create post form
    router.form("/posts/create", [&](auto& req, auto& params) {
        if (req.method_ == HttpMethod::POST) {
            auto validation = validate(req.postData_, {
                {"title", "length:1-200"},
                {"content", "length:1-10000"}
            });

            if (!validation.valid) {
                return HttpResponse().status(400).html(
                    templates.render("create", {{"errors", validation.errors}})
                );
            }

            db::db().table("posts").insert({
                {"title", validation.data["title"]},
                {"content", validation.data["content"]},
                {"author", Auth::user()["username"]}
            });

            return HttpResponse().redirect("/");
        }

        return HttpResponse().html(templates.render("create", {}));
    }).middleware(requireAuth);

    // Static files
    StaticFileHandler assets("./public");
    router.get("/static/*", [&assets](auto& req, auto& params) {
        return assets.handle(req, req.path_);
    });

    // Start server
    std::cout << "Blog running on http://localhost:8080\n";
    HttpServer server(8080, router);
    server.run();

    return 0;
}

Design Philosophy

This framework is built on a few core principles:

Simplicity over complexity: APIs should be intuitive. Common tasks should be easy. You shouldn't need to read documentation for hours to get started.

Performance matters: Use efficient algorithms (radix trie for routing, template caching) and zero-copy techniques where possible. The framework should be fast enough that it's never your bottleneck.

Security by default: Auto-escape template output, hash passwords with Argon2, use secure session IDs, prevent SQL injection with prepared statements, validate paths to prevent traversal attacks.

Zero heavy dependencies: The core framework uses only the C++ standard library and system APIs. Database drivers are the only external dependencies, and those are optional.

Modern C++, but not gratuitously: Use C++17 features where they make code clearer or safer, but don't use fancy features just to show off. The code should be readable.

What This Framework Is Good For

This framework shines when you want the performance and control of C++ but don't want to reinvent the wheel for every web app:

  • RESTful APIs that need to handle high throughput
  • Server-rendered web applications
  • Admin panels and internal tools
  • Embedded web interfaces for desktop applications
  • Games with web-based UIs or backends
  • Anywhere you need a lightweight, self-contained web server

What It's Not

This isn't a massive enterprise framework with every possible feature. It doesn't include an ORM, a job queue, a WebSocket implementation, or a thousand other things you might find in Django or Rails. It's intentionally focused on core web framework features done well.

If you need those features, you can integrate other libraries or build them yourself. The framework is designed to be a solid foundation, not a walled garden.

Contributing

Contributions are welcome, but they should align with the framework's philosophy: keep it simple, keep it fast, keep it secure. Complex features that serve edge cases probably belong in a separate library.

License

MIT License - see LICENSE file for details.