Basic Server
In the previous chapter, you sent requests from a client to a test server. Now let's walk through how that server actually works.
Starting the Server
Once you've registered your routes, call svr.listen() to start the server.
svr.listen("0.0.0.0", 8080);
svr.listen("0.0.0.0", 8080);
The first argument is the host, and the second is the port. "0.0.0.0" listens on all network interfaces. Use "127.0.0.1" if you want to accept connections from your own machine only.
listen() is a blocking call. It won't return until the server stops. The server keeps running until you press Ctrl+C in your terminal or call svr.stop() from another thread.
Routing
Routing is the heart of any server. It's how you tell cpp-httplib: when a request comes in for this URL with this HTTP method, run this code.
httplib::Server svr; svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) { res.set_content("Hello!", "text/plain"); });
httplib::Server svr; svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) { res.set_content("Hello!", "text/plain"); });
svr.Get() registers a handler for GET requests. The first argument is the path, the second is the handler function. When a GET request arrives at /hi, your lambda runs.
There's a method for each HTTP verb.
svr.Get("/path", handler); // GET svr.Post("/path", handler); // POST svr.Put("/path", handler); // PUT svr.Delete("/path", handler); // DELETE
svr.Get("/path", handler); // GET svr.Post("/path", handler); // POST svr.Put("/path", handler); // PUT svr.Delete("/path", handler); // DELETE
The handler signature is (const httplib::Request &req, httplib::Response &res). You can use auto to keep it short.
svr.Get("/hi", [](const auto &req, auto &res) { res.set_content("Hello!", "text/plain"); });
svr.Get("/hi", [](const auto &req, auto &res) { res.set_content("Hello!", "text/plain"); });
The handler only runs when the path matches. Requests to unregistered paths automatically return 404.
The Request Object
The first parameter req gives you everything the client sent.
Body
req.body holds the request body as a std::string.
svr.Post("/post", [](const auto &req, auto &res) { // Echo the body back to the client res.set_content(req.body, "text/plain"); });
svr.Post("/post", [](const auto &req, auto &res) { // Echo the body back to the client res.set_content(req.body, "text/plain"); });
Headers
Use req.get_header_value() to read a request header.
svr.Get("/check", [](const auto &req, auto &res) { auto auth = req.get_header_value("Authorization"); res.set_content("Auth: " + auth, "text/plain"); });
svr.Get("/check", [](const auto &req, auto &res) { auto auth = req.get_header_value("Authorization"); res.set_content("Auth: " + auth, "text/plain"); });
Query Parameters and Form Data
req.get_param_value() retrieves a parameter by name. It works for both GET query parameters and POST form data.
svr.Get("/search", [](const auto &req, auto &res) { auto q = req.get_param_value("q"); res.set_content("Query: " + q, "text/plain"); });
svr.Get("/search", [](const auto &req, auto &res) { auto q = req.get_param_value("q"); res.set_content("Query: " + q, "text/plain"); });
A request to /search?q=cpp-httplib gives you "cpp-httplib" for q.
To loop over all parameters, use req.params.
svr.Post("/submit", [](const auto &req, auto &res) { std::string result; for (auto &[key, val] : req.params) { result += key + " = " + val + "\n"; } res.set_content(result, "text/plain"); });
svr.Post("/submit", [](const auto &req, auto &res) { std::string result; for (auto &[key, val] : req.params) { result += key + " = " + val + "\n"; } res.set_content(result, "text/plain"); });
File Uploads
Files uploaded via multipart form data are available through req.form.get_file().
svr.Post("/upload", [](const auto &req, auto &res) { auto f = req.form.get_file("file"); auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; res.set_content(content, "text/plain"); });
svr.Post("/upload", [](const auto &req, auto &res) { auto f = req.form.get_file("file"); auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; res.set_content(content, "text/plain"); });
f.filename gives you the filename, and f.content gives you the file data.
Path Parameters
Sometimes you want to capture part of the URL as a variable -- for example, the 42 in /users/42. Use the :param syntax to do that.
svr.Get("/users/:id", [](const auto &req, auto &res) { auto id = req.path_params.at("id"); res.set_content("User ID: " + id, "text/plain"); });
svr.Get("/users/:id", [](const auto &req, auto &res) { auto id = req.path_params.at("id"); res.set_content("User ID: " + id, "text/plain"); });
A request to /users/42 gives you "42" from req.path_params.at("id"). /users/100 gives you "100".
You can capture multiple segments at once.
svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) { auto user_id = req.path_params.at("user_id"); auto post_id = req.path_params.at("post_id"); res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain"); });
svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) { auto user_id = req.path_params.at("user_id"); auto post_id = req.path_params.at("post_id"); res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain"); });
Regex Patterns
You can also write a regular expression directly in the path instead of :param. Capture group values are available via req.matches, which is a std::smatch.
// Only accept numeric IDs svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { auto id = req.matches[1]; // First capture group res.set_content("File ID: " + std::string(id), "text/plain"); });
// Only accept numeric IDs svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { auto id = req.matches[1]; // First capture group res.set_content("File ID: " + std::string(id), "text/plain"); });
/files/42 matches, but /files/abc doesn't. This is handy when you want to constrain what values are accepted.
Building a Response
The second parameter res is how you send data back to the client.
Body and Content-Type
res.set_content() sets the body and Content-Type. That's all you need for a 200 response.
svr.Get("/hi", [](const auto &req, auto &res) { res.set_content("Hello!", "text/plain"); });
svr.Get("/hi", [](const auto &req, auto &res) { res.set_content("Hello!", "text/plain"); });
Status Code
To return a different status code, assign to res.status.
svr.Get("/not-found", [](const auto &req, auto &res) { res.status = 404; res.set_content("Not found", "text/plain"); });
svr.Get("/not-found", [](const auto &req, auto &res) { res.status = 404; res.set_content("Not found", "text/plain"); });
Response Headers
Add response headers with res.set_header().
svr.Get("/with-header", [](const auto &req, auto &res) { res.set_header("X-Custom", "my-value"); res.set_content("Hello!", "text/plain"); });
svr.Get("/with-header", [](const auto &req, auto &res) { res.set_header("X-Custom", "my-value"); res.set_content("Hello!", "text/plain"); });
Walking Through the Test Server
Now let's use what we've learned to read through the test server from the previous chapter.
GET /hi
svr.Get("/hi", [](const auto &, auto &res) { res.set_content("Hello!", "text/plain"); });
svr.Get("/hi", [](const auto &, auto &res) { res.set_content("Hello!", "text/plain"); });
The simplest possible handler. We don't need any information from the request, so the req parameter is left unnamed. It just returns "Hello!".
GET /search
svr.Get("/search", [](const auto &req, auto &res) { auto q = req.get_param_value("q"); res.set_content("Query: " + q, "text/plain"); });
svr.Get("/search", [](const auto &req, auto &res) { auto q = req.get_param_value("q"); res.set_content("Query: " + q, "text/plain"); });
req.get_param_value("q") pulls out the query parameter q. A request to /search?q=cpp-httplib returns "Query: cpp-httplib".
POST /post
svr.Post("/post", [](const auto &req, auto &res) { res.set_content(req.body, "text/plain"); });
svr.Post("/post", [](const auto &req, auto &res) { res.set_content(req.body, "text/plain"); });
An echo server. Whatever body the client sends, req.body holds it, and we send it straight back.
POST /submit
svr.Post("/submit", [](const auto &req, auto &res) { std::string result; for (auto &[key, val] : req.params) { result += key + " = " + val + "\n"; } res.set_content(result, "text/plain"); });
svr.Post("/submit", [](const auto &req, auto &res) { std::string result; for (auto &[key, val] : req.params) { result += key + " = " + val + "\n"; } res.set_content(result, "text/plain"); });
Loops over the form data in req.params using structured bindings (auto &[key, val]) to unpack each key-value pair.
POST /upload
svr.Post("/upload", [](const auto &req, auto &res) { auto f = req.form.get_file("file"); auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; res.set_content(content, "text/plain"); });
svr.Post("/upload", [](const auto &req, auto &res) { auto f = req.form.get_file("file"); auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; res.set_content(content, "text/plain"); });
Receives a file uploaded via multipart form data. req.form.get_file("file") fetches the field named "file", and we respond with the filename and size.
GET /users/:id
svr.Get("/users/:id", [](const auto &req, auto &res) { auto id = req.path_params.at("id"); res.set_content("User ID: " + id, "text/plain"); });
svr.Get("/users/:id", [](const auto &req, auto &res) { auto id = req.path_params.at("id"); res.set_content("User ID: " + id, "text/plain"); });
:id is the path parameter. req.path_params.at("id") retrieves its value. /users/42 gives you "42", /users/alice gives you "alice".
GET /files/(\d+)
svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { auto id = req.matches[1]; res.set_content("File ID: " + std::string(id), "text/plain"); });
svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { auto id = req.matches[1]; res.set_content("File ID: " + std::string(id), "text/plain"); });
The regex (\d+) matches numeric IDs only. /files/42 hits this handler, but /files/abc returns 404. req.matches[1] retrieves the first capture group.
Next Steps
You now have the full picture of how a server works. Routing, reading requests, building responses -- that's enough to build a real API server.
Next, let's look at serving static files. We'll build a server that delivers HTML and CSS.
Next: Static File Server