InAPI is a C++ library for building HTTP app with FastAPI-like syntax
Internally cpp-httplib and nlohmann/json are used
- Quick Start
- Build
- Include
- App
- Routes
- Request
- Response
- JSON
- Path Parameters
- Query Parameters
- Headers and Cookies
- Forms and File Uploads
- Middleware
- Router
- CORS
- Authorization
- Validation
- Error Handling
- Static Files
- OpenAPI and Swagger
- Config
- SSL
- Logging
- Full Example
- Cheat Sheet
#include <InAPI.hpp>
int main() {
App app;
app.get("/", [] {
return text("Hello from InAPI");
});
app.run(8080);
}The repository includes a makefile for building the example from src/main.cpp. It uses g++ and places the result in the build directory.
Regular build without running:
make compileBuild and run the regular example:
makeBuild a custom source file instead of src/main.cpp:
make compile SRC=src/app.cppBuild and run a custom source file:
make SRC=src/app.cppBuild with OpenSSL without running:
make compile_sslBuild and run the SSL variant:
make sslRun an already built regular binary:
make runClean built binaries:
make cleanBuild requirements:
- a compiler with C++17 support or newer, such as
g++; make;- for SSL builds, installed OpenSSL headers and libraries (
sslandcrypto).
If your compiler has a different name, pass it through the CXX variable:
make compile CXX=clang++Include the main header:
#include <InAPI.hpp>If you include headers manually, the files are located in include/InAPI.
App is the main application class. It is used to add routes, middleware, CORS, authorization, error handlers, static files, and Swagger.
App app;Main methods:
| Method | Purpose |
|---|---|
get(path, handler) |
Registers a GET route |
post(path, handler) |
Registers a POST route |
put(path, handler) |
Registers a PUT route |
patch(path, handler) |
Registers a PATCH route |
del(path, handler) |
Registers a DELETE route |
options(path, handler) |
Registers an OPTIONS route |
middleware(handler) |
Adds global middleware |
include(prefix, router) |
Includes a Router with a prefix |
Cors(options) |
Enables CORS |
BearerAuth(token) |
Enables Bearer authorization by token |
BearerAuth(hook) |
Enables Bearer authorization through a function |
error_handler(status, handler) |
Adds an HTTP error handler |
exception_handler(handler) |
Adds an exception handler |
mount(path, directory) |
Serves static files from a directory |
run(...) |
Starts the server |
A handler can accept Request:
app.get("/hello", [](Request request) {
return text("Path: " + request.path());
});If request data is not needed, the handler can have no arguments:
app.get("/", []() {
return text("Home");
});Supported HTTP methods:
app.get("/items", handler);
app.post("/items", handler);
app.put("/items/{id:int}", handler);
app.patch("/items/{id:int}", handler);
app.del("/items/{id:int}", handler);
app.options("/items", handler);Every handler must return Response.
Request contains the incoming HTTP request data.
Main methods:
| Method | Returns |
|---|---|
method() |
HTTP method |
path() |
Request path |
body() |
Request body as a string |
json() |
Request body as nlohmann::json |
body(schema) |
JSON body after validation |
header(name) |
Header value |
has_header(name) |
Whether the header exists |
query(name) |
Query parameter |
query_or(name, default_value) |
Query parameter or default value |
query_int(name, default_value) |
Query parameter as int |
query_bool(name, default_value) |
Query parameter as bool |
has_query(name) |
Whether the query parameter exists |
query(schema) |
Query parameters after validation |
param(name) |
Path parameter |
param_int(name) |
Path parameter as int |
has_param(name) |
Whether the path parameter exists |
params(schema) |
Path parameters after validation |
cookie(name) |
Cookie |
has_cookie(name) |
Whether the cookie exists |
form(name) |
Form field |
form_or(name, default_value) |
Form field or default value |
has_form(name) |
Whether the form field exists |
has_file(name) |
Whether a file exists |
file(name) |
One uploaded file |
files() |
All uploaded files |
files(name) |
All files with the specified field name |
bearer_token() |
Bearer token from Authorization |
basic_auth() |
Basic Auth data |
content_type() |
Content-Type |
user_agent() |
User-Agent |
ip() |
Client IP |
port() |
Client port |
http_version() |
HTTP version |
Response describes the server response.
return Response(200, "OK", "text/plain; charset=utf-8");It is usually more convenient to use helper functions:
| Helper | Purpose |
|---|---|
text(body, status = 200) |
Text response |
html(body, status = 200) |
HTML response |
json(body, status = 200) |
JSON response |
redirect(url, status = 302) |
Redirect with the Location header |
status(code) |
Empty response with the specified status |
file(path, status = 200) |
File response |
error(status) |
JSON error by status |
error(status, message) |
JSON error with text |
Examples:
app.get("/text", []() {
return text("Hello");
});
app.get("/html", []() {
return html("<h1>Hello</h1>");
});
app.get("/json", []() {
return json({{"ok", true}});
});
app.get("/redirect", []() {
return redirect("/json");
});
app.get("/empty", []() {
return status(204);
});app.get("/headers", []() {
Response response = json({{"ok", true}});
response.header("X-App", "InAPI");
return response;
});app.get("/login", []() {
Response response = json({{"logged", true}});
response.set_cookie("token", "abc123");
return response;
});
app.get("/logout", []() {
Response response = redirect("/");
response.delete_cookie("token");
return response;
});set_cookie accepts these arguments:
set_cookie(name, value, path, max_age, http_only, secure, same_site)Default values:
path = "/"max_age = -1http_only = truesecure = falsesame_site = "Lax"
InAPI uses nlohmann::json.
app.post("/echo", [](Request request) {
Json body = request.json();
return json({
{"you_sent", body}
});
});If the request body contains invalid JSON, request.json() throws nlohmann::json::parse_error.
Use request.body(schema) for an automatic 422 response.
Path parameters are written in curly braces:
app.get("/users/{id}", [](Request request) {
return text(request.param("id"));
});Supported types:
| Syntax | Accepts |
|---|---|
{name} |
Any text up to / |
{name:int} |
Integer, including a negative one |
{name:path} |
The rest of the path, including / |
Examples:
app.get("/users/{id:int}", [](Request request) {
int id = request.param_int("id");
return json({{"id", id}});
});
app.get("/files/{path:path}", [](Request request) {
return json({{"path", request.param("path")}});
});app.get("/search", [](Request request) {
std::string q = request.query_or("q", "");
int page = request.query_int("page", 1);
bool debug = request.query_bool("debug", false);
return json({
{"q", q},
{"page", page},
{"debug", debug}
});
});query_bool accepts these values:
true:1,true,yes,onfalse:0,false,no,off
app.get("/client", [](Request request) {
return json({
{"user_agent", request.user_agent()},
{"content_type", request.content_type()},
{"session", request.cookie("session")},
{"has_session", request.has_cookie("session")}
});
});Bearer token:
app.get("/token", [](Request request) {
auto token = request.bearer_token();
if (!token) {
return error(401);
}
return json({{"token", *token}});
});Basic Auth:
app.get("/basic", [](Request request) {
auto auth = request.basic_auth();
if (!auth) {
return error(401);
}
return json({
{"username", auth->username}
});
});Form fields:
app.post("/form", [](Request request) {
std::string name = request.form_or("name", "anonymous");
return json({
{"name", name}
});
});One file:
app.post("/upload", [](Request request) {
if (!request.has_file("file")) {
return error(400, "File is required");
}
UploadedFile uploaded = request.file("file");
uploaded.save("uploads/" + uploaded.filename);
return json({
{"name", uploaded.name},
{"filename", uploaded.filename},
{"content_type", uploaded.content_type},
{"size", uploaded.size()}
});
});Multiple files:
app.post("/uploads", [](Request request) {
Json result = Json::array();
for (const UploadedFile& uploaded : request.files("files")) {
result.push_back({
{"filename", uploaded.filename},
{"size", uploaded.size()}
});
}
return json(result);
});UploadedFile contains:
| Field or Method | Purpose |
|---|---|
name |
Form field name |
filename |
File name |
content_type |
MIME type |
content |
File contents |
headers |
Form part headers |
size() |
Content size |
empty() |
Checks whether the file is empty |
save(path) |
Saves the file |
Middleware runs before the route handler. It can:
- pass the request forward through
next(request); - return a response immediately;
- modify the response after the handler has run.
app.middleware([](Request request, Next next) {
Response response = next(request);
response.header("X-Powered-By", "InAPI");
return response;
});Header check:
app.middleware([](Request request, Next next) {
if (request.header("X-API-Key") != "secret") {
return error(401, "Invalid API key");
}
return next(request);
});Execution order:
- Global application middleware.
- Router middleware, if the route is included through
include. - Route handler.
Router helps split an API into route groups.
Router users;
users.get("/", []() {
return json({{"items", Json::array()}});
});
users.get("/{id:int}", [](Request request) {
return json({{"id", request.param_int("id")}});
});
app.include("/users", users);Final paths:
GET /usersGET /users/{id:int}
A router can have its own middleware:
Router admin;
admin.middleware([](Request request, Next next) {
if (request.header("X-Admin") != "true") {
return forbidden();
}
return next(request);
});
admin.get("/stats", []() {
return json({{"users", 100}});
});
app.include("/admin", admin);The shortest option:
app.Cors();Default values:
- origins:
* - methods:
GET,POST,PUT,PATCH,DELETE,OPTIONS - headers:
Content-Type,Authorization
Configuration:
app.Cors(CorsOptions(
{"https://example.com"},
{"GET", "POST"},
{"Content-Type", "Authorization", "X-API-Key"}
));InAPI answers preflight OPTIONS requests automatically.
app.BearerAuth("secret-token");After this, the request must pass the header:
Authorization: Bearer secret-tokenapp.BearerAuth([](Request request) {
auto token = request.bearer_token();
return token && token->size() > 10;
});Router api;
api.BearerAuth("router-token");
api.get("/private", []() {
return json({{"private", true}});
});
app.include("/api", api);For Basic Auth, use the require_auth middleware and the basic_auth helper function.
app.middleware(require_auth(
basic_auth("admin", "password"),
"Basic"
));return unauthorized();
return unauthorized("Login required", "Bearer");
return forbidden();
return forbidden("Access denied");Validation is built through ValidationSchema and field.
ValidationSchema user_schema = {
field("name").string().required().min_len(2).max_len(50),
field("age").integer().optional().min(0).max(150),
field("email").string().required().email()
};app.post("/users", [](Request request) {
ValidationSchema schema = {
field("name").string().required().min_len(2),
field("email").string().required().email(),
field("age").integer().default_value(18)
};
Json body = request.body(schema);
return json({
{"created", true},
{"user", body}
}, 201);
});If validation fails, InAPI returns 422:
{
"error": "Validation failed",
"details": [
{
"field": "email",
"code": "invalid_email",
"message": "Invalid email"
}
]
}app.get("/search", [](Request request) {
ValidationSchema schema = {
field("q").string().required(),
field("page").integer().default_value(1).min(1),
field("active").boolean().default_value(true)
};
Json query = request.query(schema);
return json(query);
});app.get("/users/{id}", [](Request request) {
ValidationSchema schema = {
field("id").integer().required().min(1)
};
Json params = request.params(schema);
return json({
{"id", params["id"]}
});
});| Rule | Purpose |
|---|---|
string() |
String |
integer() |
Integer |
number() |
Number |
boolean() |
Boolean |
array() |
Array |
array(field(...)) |
Array of elements validated by a rule |
object({...}) |
Object with nested fields |
required() |
Field is required |
optional() |
Field is optional |
nullable() |
Allows null |
default_value(value) |
Default value |
min(value) |
Minimum number |
max(value) |
Maximum number |
min_len(value) |
Minimum string or array length |
max_len(value) |
Maximum string or array length |
one_of({...}) |
One of the allowed values |
regex(pattern) |
Regular expression check |
email() |
|
url() |
URL |
uuid() |
UUID |
custom(message, callback) |
Custom validation |
ValidationSchema schema = {
field("title").string().required(),
field("tags").array(field("").string().min_len(2)),
field("author").object({
field("name").string().required(),
field("email").string().email()
}).required()
};ValidationSchema schema = {
field("password").string().required().custom(
"Password must contain at least one digit",
[](const Json& value) {
std::string password = value.get<std::string>();
return password.find_first_of("0123456789") != std::string::npos;
}
)
};app.error_handler(404, [](Request request) {
return json({
{"error", "Route not found"},
{"path", request.path()}
}, 404);
});If a status has no custom handler, InAPI returns standard JSON:
{
"error": "Not found"
}Standard messages exist for these statuses:
400 Bad request401 Unauthorized403 Forbidden404 Not found405 Method not allowed409 Conflict413 Payload too large422 Unprocessable entity500 Internal server error502 Bad gateway503 Service unavailable
app.exception_handler([](const std::exception& exception) {
return json({
{"error", exception.what()}
}, 500);
});ValidationException is handled separately and becomes a 422 response.
app.mount("/static", "public");After this, files from the public directory are available under /static.
Behavior:
- if a directory is requested, InAPI looks for
index.html; - if a file is not found, InAPI tries to serve
index.htmlfrom the directory root, which is useful for SPAs; - the path is protected from leaving the directory through
..; - static files get
Cache-Control,ETag, andLast-Modified; If-None-MatchandIf-Modified-Sinceare supported with a304response.
InAPI can generate an OpenAPI document and Swagger UI.
Swagger swagger(
true,
"/docs",
"Users API",
"1.0.0",
"Example InAPI documentation"
);
app.run("0.0.0.0", 8080, Config(), swagger);After startup, these are available:
- Swagger UI:
/docs - OpenAPI JSON:
/docs/openapi.json
Route methods return RouteDoc, so descriptions can be added as a chain:
app.get("/users/{id:int}", [](Request request) {
return json({
{"id", request.param_int("id")},
{"name", "Marat"}
});
})
.summary("Get user by id")
.tag("Users")
.response(200, "OK")
.response(404, "User not found");A type can describe an OpenAPI schema through static methods:
struct User {
static std::string openapi_name() {
return "User";
}
static Json openapi_schema() {
return {
{"type", "object"},
{"properties", {
{"id", {{"type", "integer"}}},
{"name", {{"type", "string"}}}
}},
{"required", {"id", "name"}}
};
}
};Usage:
app.get("/users/{id:int}", [](Request request) {
return json({
{"id", request.param_int("id")},
{"name", "Marat"}
});
})
.summary("Get user by id")
.tag("Users")
.response<User>(200)
.response(404, "User not found");If openapi_name() is not specified, the name is taken from the type. If openapi_schema() is not specified, the schema will be:
{
"type": "object"
}If app.BearerAuth(...) is enabled, Swagger automatically gets the BearerAuth scheme.
For an individual route:
app.get("/private", []() {
return json({{"private", true}});
})
.bearer_auth()
.summary("Private route");Bearer auth can also be enabled at the Swagger level:
Swagger swagger(true, "/docs", "API", "1.0.0", "", true);Config configures the server.
Config config(
true,
8,
"10mb",
5,
10,
30
);Parameters:
| Parameter | Value |
|---|---|
logger |
Enables logging |
threads |
Number of threads |
max_body_size |
Maximum request body size |
read_timeout_seconds |
Read timeout |
write_timeout_seconds |
Write timeout |
idle_timeout_seconds |
Keep-Alive idle timeout |
ssl |
SSL settings |
Default values:
Config(
true,
auto_threads(),
"1mb",
5,
10,
30,
std::nullopt
);Supported max_body_size values:
512kb1mb10mb1gb
If a different value is specified, 1mb will be used.
Run variants:
app.run(8080);
app.run(8080, "127.0.0.1");
app.run("127.0.0.1", 8080);
app.run("0.0.0.0", 8080, config);
app.run("0.0.0.0", 8080, config, swagger);SSL is configured through SSL inside Config.
Config config(
true,
8,
"10mb",
5,
10,
30,
SSL("cert.pem", "key.pem")
);
app.run("0.0.0.0", 443, config);Important:
- for SSL, the library must be built with
CPPHTTPLIB_OPENSSL_SUPPORT; - OpenSSL libraries are also required;
- if the certificate or key is not found, InAPI throws an exception.
Logging is enabled by default.
InAPI writes:
- a message when the server starts;
- one line for each request;
- the HTTP status in color if the terminal supports ANSI colors.
Disable logging:
Config config(false);
app.run("0.0.0.0", 8080, config);Manual message:
InAPILogger::info("Server is ready");#include <InAPI.hpp>
struct User {
static std::string openapi_name() {
return "User";
}
static Json openapi_schema() {
return {
{"type", "object"},
{"properties", {
{"id", {{"type", "integer"}}},
{"name", {{"type", "string"}}},
{"email", {{"type", "string"}}}
}},
{"required", {"id", "name", "email"}}
};
}
};
int main() {
App app;
app.Cors();
app.middleware([](Request request, Next next) {
Response response = next(request);
response.header("X-App", "InAPI");
return response;
});
Router users;
users.get("/{id:int}", [](Request request) {
int id = request.param_int("id");
return json({
{"id", id},
{"name", "Marat"},
{"email", "marat@example.com"}
});
})
.summary("Get user by id")
.tag("Users")
.response<User>(200)
.response(404, "User not found");
users.post("/", [](Request request) {
ValidationSchema schema = {
field("name").string().required().min_len(2),
field("email").string().required().email()
};
Json body = request.body(schema);
return json({
{"created", true},
{"user", body}
}, 201);
})
.summary("Create user")
.tag("Users")
.response<User>(201)
.response(422, "Validation failed");
app.include("/users", users);
app.error_handler(404, [](Request request) {
return json({
{"error", "Route not found"},
{"path", request.path()}
}, 404);
});
app.exception_handler([](const std::exception& exception) {
return json({
{"error", exception.what()}
}, 500);
});
Config config(
true,
8,
"10mb",
5,
10,
30
);
Swagger swagger(
true,
"/docs",
"Users API",
"1.0.0",
"Example InAPI documentation"
);
app.run("0.0.0.0", 8080, config, swagger);
}App app;
app.get("/", []() {
return text("Hello");
});
app.post("/users", [](Request request) {
Json body = request.json();
return json(body, 201);
});
app.get("/users/{id:int}", [](Request request) {
return json({{"id", request.param_int("id")}});
});
Router api;
api.get("/health", []() {
return json({{"ok", true}});
});
app.include("/api", api);
app.Cors();
app.BearerAuth("secret-token");
app.mount("/static", "public");
app.error_handler(404, [](Request request) {
return error(404, "Route not found");
});
app.run(8080);