- C++ 100%
| docs | ||
| src | ||
| test | ||
| web | ||
| .gitignore | ||
| CLAUDE.md | ||
| coding_style.md | ||
| lobster.md | ||
| Makofile | ||
| plan.md | ||
| README.md | ||
| web.hpp | ||
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.