A fast, lightweight web framework for building REST APIs in C3.
Eclair aims to make building HTTP APIs in C3 as simple and intuitive as possible, drawing inspiration from modern web frameworks like FastAPI (Python), rocket.rs (Rust), and Express.js (JavaScript). While it takes ideas from these frameworks, eclair is designed specifically for C3 with its own unique approach to routing, middleware, and request handling.
The framework leverages C3's powerful macro system to provide a declarative, attribute-based routing mechanism that feels natural to the language. Whether you're building a small microservice or a larger API, eclair gives you the tools to get started quickly without sacrificing performance.
- Declarative Routing: Define routes using the
@Routeattribute, similar to decorators in FastAPI or attributes in rocket.rs - Automatic Parameter Inference: Simply add parameters to your handler function - types like
int id,String name, orbool completedare automatically extracted from path segments, query parameters, or request bodies - Automatic Response Handling: Functions returning
Stringor serializable types are automatically sent as JSON responses - Path Parameters: Capture dynamic segments from URLs - just add a matching parameter (e.g.,
int idfor/todos/:id) - Query Parameters: Access query string parameters - add parameters like
Maybe{bool} completedfor optional query params - JSON Body Parsing: Automatically deserialize JSON request bodies - add a struct parameter and it's automatically parsed
- Full request control: Use a
Request*parameter to control everything yourself - Maybe Types: Use
Maybe{T}for optional query parameters andMaybe{T}for optional return values (returns JSON or null) - Fault Handling: Make your return type
T?for error responses with automatic 500 error handling - Routers: Group routes under a common prefix and include them in the server (sub-routers also supported)
- HTTP Method Support: Support for all standard HTTP methods (GET, POST, PUT, DELETE, PATCH, etc.)
- C Interop: Built on top of httpserver.h, a minimal C HTTP server library
module example;
import std::io;
import eclair;
fn String hello() @Route({ GET, "/hello" }) {
return "Hello world!\n";
}
fn void api_status(Request* req, Response* res) @Route({ GET, "/status" }) {
res.set_body("OK");
}
fn int main(String[] args) {
Server server = eclair::new_server();
server.@add_route(hello);
server.@add_route(api_status);
server.listen();
return 0;
}import eclair; is enough to get Server, Router, Request, Response and the @Route attribute.
Make sure you have the C3 compiler installed, version 0.8.4 or later.
Using c3po
In your project, run
c3po add ecoral360/eclairThis installs eclair into lib/eclair.c3l, pulls dessert (the serialization library eclair uses) as a transitive dependency, and adds both to your project.json.
- Run
c3c init <YOUR_PROJECT> - Clone this repository into
<YOUR_PROJECT>/lib/eclair.c3l - Clone the dessert repository into
<YOUR_PROJECT>/lib/dessert.c3l - Add
"dependencies": ["eclair", "dessert"]to yourproject.json - You are done ! Enjoy writing your REST API in C3 !
Eclair ships prebuilt target configurations for Linux (x64, x86, aarch64, riscv32/64), macOS (x64, aarch64), FreeBSD, NetBSD, OpenBSD and Android (aarch64). Windows is not supported yet, since the underlying httpserver.h backend relies on epoll/kqueue.
This repository contains a small example server behind the ECLAIR_EXAMPLE feature flag (src/main.c3):
c3c run example # listens on http://localhost:8080, try /hello/world?greeting=HiThe Server struct is the core of eclair. Create a new server with a port number (defaults to 8080):
Server server = eclair::new_server(); // port will be 8080
Server server = eclair::new_server(5444); // port will be 5444server.listen() prints every registered route, then blocks and serves requests.
Routes are defined using the @Route attribute with an HTTP method and path. The macro system automatically handles parameter inference:
fn String my_handler() @Route({ GET, "/path" }) {
return "response";
}Handlers are registered with server.@add_route(handler) (or router.@add_route(handler)), which reads the @Route tag at compile time.
Handler functions can have parameters automatically inferred:
Request*- The request objectResponse*- The response objectint,String,bool,char- Path or query parameters (matching:paramin path orparamin query string)- Any deserializable type (
deserializable struct,Maybe{deserializable struct},List{deserializable struct}, ordeserializable struct[]) - Automatically parsed from the request body (see dessert for more info on deserializable types) Maybe{int | String | bool | char}- Optional query parameters
Path parameters take precedence over query parameters: a parameter is first looked up in req.params, then in req.query.
Handler functions can return different types:
String- Automatically set as the response body- Any serializable type (
serializable struct,Maybe{serializable struct},List{serializable struct}, orserializable struct[]) - Automatically serialized to JSON (see dessert for more info on serializable types) Maybe{T}- Returns the value as JSON, ornullif emptyT?- Returns the value as JSON, or 500 error if the handler returns afaultvoid/void?- Lets the handler function deal with the response
Unmatched targets get an automatic 404.
Eclair uses dessert for JSON. Derive the serialize / deserialize methods on the structs you send or receive:
import dessert;
$expand(dessert::@derive(Todo));
struct Todo {
int id;
String title;
bool completed;
}Pass serialize or deserialize as a second argument to derive only one of them.
Capture dynamic segments from URLs automatically by adding a matching parameter:
fn Maybe{Todo} get_todo(int id) @Route({ GET, "/todos/:id" }) {
foreach (todo : todos) {
if (todo.id == id) {
return maybe::value{Todo}(todo); // Wrap value in Maybe
}
}
return {}; // Empty Maybe returns null
}Access query string parameters automatically using parameters. Use Maybe{T} for optional params:
fn List{Todo} get_todos(Maybe{bool} completed) @Route({ GET, "/todos" }) {
// completed is an optional query parameter (?completed=true)
// Access with `if (try value = completed.get()) { ... }` or `completed.get() ?? default_value`
}A flag without a value (?completed) is stored with the flag name as its value.
Request bodies are automatically deserialized from JSON by adding a deserializable struct parameter:
fn Todo? create_todo(TodoInput input) @Route({ POST, "/todos" }) {
// TodoInput is automatically parsed from the request body
// ...
}Group routes under a common prefix using routers:
fn void main() {
// ...
Router todo_router = router::new_router("/todos");
todo_router.@add_route(get_todos);
todo_router.@add_route(create_todo);
todo_router.@add_route(get_todo);
todo_router.@add_route(update_todo);
todo_router.@add_route(delete_todo);
server.include_router(todo_router);
}Routers nest: use parent_router.include_router(child_router) to build a prefix tree. A router's route paths are relative to its prefix.
The Request and Response types give you access to the HTTP transaction:
fn void handler(Request* req, Response* res) @Route({ GET, "/example" }) {
// Access request data
String target = req.target();
HTTPMethod method = req.method();
String body = req.body();
String id = req.params["id"]!!; // path parameters
String q = req.query["q"]!!; // query parameters
// Deserialize the body yourself
MyStruct? parsed = req.json_body_as(MyStruct);
// Set response
res.set_status(200);
res.set_body("Hello, world!");
}Return faults for error responses. The framework automatically handles them:
fn Todo? get_todo(int id) @Route({ GET, "/todos/:id" }) {
foreach (todo : todos) {
if (todo.id == id) {
return todo;
}
}
return NOT_FOUND~; // Returns 500 error
}A parameter that fails to parse (e.g. abc for an int id) is left at its zero value and logged on stderr; wrap it in Maybe{T} if you want to detect that yourself.
See todo.c3 for a full REST API example with CRUD operations:
module example::todo;
import std;
import eclair;
import dessert;
$expand(dessert::@derive(Todo));
struct Todo {
int id;
String title;
bool completed;
}
$expand(dessert::@derive(TodoInput));
struct TodoInput {
String title;
bool completed;
}
// Query parameter - optional (?completed=true or ?completed=false)
fn List{Todo} get_todos(Maybe{bool} completed) @Route({ GET, "/" }) { /* ... */ }
// Body parameter - inferred from JSON request body
fn Todo? create_todo(TodoInput input) @Route({ POST, "/" }) { /* ... */ }
// Path parameter - matches :id in route
fn Maybe{Todo} get_todo(int id) @Route({ GET, "/:id" }) { /* ... */ }
// Multiple parameters combined
fn Todo? update_todo(int id, TodoInput input) @Route({ PUT, "/:id" }) { /* ... */ }
// Manual approach still supported
fn Todo? delete_todo(Request* req) @Route({ DELETE, "/:id" }) {
int id = req.params["id"].to_int()!;
// ...
}
fn String hello(Maybe{String} name) @Route({ GET, "/hello" }) {
return string::tformat("Hello, %s!", name.get() ?? "Stranger");
}
fn int main(String[] args) {
Server server = eclair::new_server();
server.@add_route(hello);
Router todo_router = router::new_router("/todos");
todo_router.@add_route(get_todos);
todo_router.@add_route(create_todo);
todo_router.@add_route(get_todo);
todo_router.@add_route(update_todo);
todo_router.@add_route(delete_todo);
server.include_router(todo_router);
server.listen();
return 0;
}curl localhost:8080/hello?name=Bob # Hello, Bob!
curl -X POST localhost:8080/todos -d '{"title":"buy milk","completed":false}'
# {"id":1,"title":"buy milk","completed":false}
curl localhost:8080/todos # [{"id":1,"title":"buy milk","completed":false}]
curl localhost:8080/todos/99 # nullEclair uses httpserver.h as its HTTP server backend. This is a minimal, fast C library that handles the low-level socket communication and HTTP parsing, allowing eclair to focus on providing a pleasant API surface.
Eclair has two meaning.
- It means lightning in French and represents the goal of making it as fast to write in as possible.
- The deserializer and serializer lib used by eclair is dessert and
eclair (au chocolat)is a pastry.
MIT