Building Modern Web Services in C with Papago

bdowns3281 pts0 comments

Building Modern Web Services in C: A Deep Dive into libpapago and libmaple | by Brian Downs | May, 2026 | MediumSitemapOpen in appSign up<br>Sign in

Medium Logo

Get app<br>Write

Search

Sign up<br>Sign in

Press enter or click to view image in full size

Photo by Jefferson Sees on UnsplashBuilding Modern Web Services in C: A Deep Dive into libpapago and libmaple

Brian Downs

8 min read·<br>2 hours ago

Listen

Share

When most developers think “web framework,” they think Node.js, Go, Python, or Rust. Rarely does C come to mind. But what if you need the raw performance and control of C with the ergonomics of a modern web framework? That’s exactly what libpapago delivers — and its companion template engine, libmaple , makes it a complete solution for server-side web development in pure C.

Why a Web Framework in C?<br>C isn’t the first language you’d reach for when building a web service — but there are compelling scenarios where it makes perfect sense: embedded systems, latency-critical APIs, legacy codebases, or situations where you simply need the smallest possible binary with the fewest possible runtime dependencies. libpapago was designed with exactly those constraints in mind: a framework that is full-featured and powerful, yet extremely simple to use.

libpapago: A Modern C Web Framework<br>Repository: github.com/briandowns/libpapago<br>License: BSD 2-Clause<br>Getting Started<br>Building libpapago is straightforward. The framework has a small, focused set of dependencies:<br>libmicrohttpd — the embedded HTTP server<br>libwebsockets — for WebSocket support<br>openssl — TLS/SSL support<br>libmaple — the Maple template engine (optional, enabled at compile time)<br>make<br>sudo make installIf you want HTML templating via Maple, enable it during the build:<br>make PAPAGO_USE_MAPLE=1<br>sudo make install PAPAGO_USE_MAPLE=1You can also build the WebSocket client library alongside the main framework:<br>make PAPAGO_WITH_WSC=1<br>sudo make install PAPAGO_WITH_WSC=1

Hello, World<br>Papago’s API is clean and minimal. Here’s a complete HTTP server that responds to GET /hello with a JSON payload:<br>#include<br>#include "papago.h"void<br>hello_handler(papago_request_t *req, papago_response_t *res, void *user_data)<br>PAPAGO_UNUSED(req);<br>PAPAGO_UNUSED(user_data); papago_res_json(res, "{\"message\":\"Hello, World!\"}");<br>}int<br>main(void)<br>papago_t *server = papago_new(); papago_config_t config = papago_default_config();<br>config.port = 8080;<br>papago_configure(server, &config); papago_route(server, PAPAGO_GET, "/hello", hello_handler, NULL); papago_start(server); // blocking papago_destroy(server);<br>return 0;<br>}Compile and run:<br>cc -o hello hello.c papago.c -lwebsockets -lmicrohttpd -lpthread \<br>-lssl -lcrypto -lz -lm<br>./helloTest it:<br>curl http://localhost:8080/hello<br># {"message":"Hello, World!"}That’s a working JSON API in under 30 lines of C. Let’s now walk through each feature in depth.

Feature Walkthrough<br>1. RESTful Routing<br>Papago supports all the standard HTTP verbs — GET, POST, PUT, DELETE, and PATCH. Routes are registered with a simple, consistent function signature:<br>papago_route(server, PAPAGO_GET, "/", index_handler, NULL);<br>papago_route(server, PAPAGO_POST, "/users", create_user, NULL);<br>papago_route(server, PAPAGO_PUT, "/users/:id", update_user, NULL);<br>papago_route(server, PAPAGO_DELETE, "/users/:id", delete_user, NULL);Every handler receives the same three arguments: a pointer to the request, a pointer to the response, and an optional void *user_data pointer you can use to pass application context (a database handle, configuration struct, etc.) without needing to rely on globals.<br>2. Dynamic Path Parameters<br>Routes can include named parameters prefixed with :. These are accessible inside handlers via papago_req_param():<br>void<br>user_handler(papago_request_t *req, papago_response_t *res, void *user_data)<br>const char *id = papago_req_param(req, "id");<br>// id now holds the value from the URL, e.g. "42"<br>}papago_route(server, PAPAGO_GET, "/users/:id", user_handler, NULL);3. Wildcard URIs<br>For broader pattern matching — useful for API versioning or catch-all routes — Papago supports wildcard URIs:<br>papago_route(server, PAPAGO_GET, "/api/v1/*", api_handler, NULL);This route will match any path under /api/v1/, giving you flexibility without needing to enumerate every sub-path.<br>4. Query Parameters<br>URL query strings are parsed automatically. Retrieve values with papago_req_query():<br>void<br>search_handler(papago_request_t *req, papago_response_t *res, void *user_data)<br>const char *q = papago_req_query(req, "q");<br>const char *page = papago_req_query(req, "page");<br>// handle search query and pagination<br>}papago_route(server, PAPAGO_GET, "/search", search_handler, NULL);A request to /search?q=libpapago&page=2 would yield "libpapago" and "2" respectively.<br>5. Request & Response API<br>Papago exposes a full, ergonomic API for working with both the inbound request and outbound response within any handler:<br>void<br>handler(papago_request_t *req, papago_response_t *res, void *user_data)<br>// Read from the request<br>const char...

server void libpapago hello papago_route null

Related Articles