InAPI - С++ библиотека для создания HTTP приложений с FastAPI-подобным синтаксисом
Внутри используются cpp-httplib и nlohmann/json
- Быстрый старт
- Сборка
- Подключение
- App
- Маршруты
- Request
- Response
- JSON
- Параметры пути
- Query-параметры
- Заголовки и cookies
- Формы и загрузка файлов
- Middleware
- Router
- CORS
- Авторизация
- Валидация
- Обработка ошибок
- Статические файлы
- OpenAPI и Swagger
- Config
- SSL
- Логирование
- Полный пример
- Краткая шпаргалка
#include <InAPI.hpp>
int main() {
App app;
app.get("/", [] {
return text("Hello from InAPI");
});
app.run(8080);
}В репозитории есть makefile для сборки примера из src/main.cpp. Он использует g++ и складывает результат в папку build.
Обычная сборка без запуска:
make compileСборка и запуск обычного примера:
makeСборка своего файла вместо src/main.cpp:
make compile SRC=src/app.cppСборка и запуск своего файла:
make SRC=src/app.cppСборка с OpenSSL без запуска:
make compile_sslСборка и запуск SSL-варианта:
make sslЗапуск уже собранного обычного бинарника:
make runОчистка собранных бинарников:
make cleanДля сборки нужны:
- компилятор с поддержкой C++17 или новее, например
g++; make;- для SSL-сборки - установленные заголовки и библиотеки OpenSSL (
sslиcrypto).
Если компилятор называется иначе, его можно передать через переменную CXX:
make compile CXX=clang++Подключите основной заголовок:
#include <InAPI.hpp>Если подключаете заголовки вручную, файлы лежат в include/InAPI.
App - основной класс приложения. Через него добавляют маршруты, middleware, CORS, авторизацию, обработчики ошибок, статические файлы и Swagger.
App app;Основные методы:
| Метод | Назначение |
|---|---|
get(path, handler) |
Регистрирует GET маршрут |
post(path, handler) |
Регистрирует POST маршрут |
put(path, handler) |
Регистрирует PUT маршрут |
patch(path, handler) |
Регистрирует PATCH маршрут |
del(path, handler) |
Регистрирует DELETE маршрут |
options(path, handler) |
Регистрирует OPTIONS маршрут |
middleware(handler) |
Добавляет глобальный middleware |
include(prefix, router) |
Подключает Router с префиксом |
Cors(options) |
Включает CORS |
BearerAuth(token) |
Включает Bearer авторизацию по токену |
BearerAuth(hook) |
Включает Bearer авторизацию через функцию |
error_handler(status, handler) |
Добавляет обработчик HTTP ошибки |
exception_handler(handler) |
Добавляет обработчик исключений |
mount(path, directory) |
Отдает статические файлы из папки |
run(...) |
Запускает сервер |
Обработчик может принимать Request:
app.get("/hello", [](Request request) {
return text("Path: " + request.path());
});Если данные запроса не нужны, обработчик может быть без аргументов:
app.get("/", []() {
return text("Home");
});Поддерживаемые HTTP методы:
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);Каждый обработчик должен вернуть Response.
Request содержит данные входящего HTTP запроса.
Основные методы:
| Метод | Что возвращает |
|---|---|
method() |
HTTP метод |
path() |
Путь запроса |
body() |
Тело запроса строкой |
json() |
Тело запроса как nlohmann::json |
body(schema) |
JSON body после валидации |
header(name) |
Значение заголовка |
has_header(name) |
Есть ли заголовок |
query(name) |
Query-параметр |
query_or(name, default_value) |
Query-параметр или значение по умолчанию |
query_int(name, default_value) |
Query-параметр как int |
query_bool(name, default_value) |
Query-параметр как bool |
has_query(name) |
Есть ли query параметр |
query(schema) |
Query-параметры после валидации |
param(name) |
Параметр пути |
param_int(name) |
Параметр пути как int |
has_param(name) |
Есть ли параметр пути |
params(schema) |
Параметры пути после валидации |
cookie(name) |
Cookie |
has_cookie(name) |
Есть ли cookie |
form(name) |
Поле формы |
form_or(name, default_value) |
Поле формы или значение по умолчанию |
has_form(name) |
Есть ли поле формы |
has_file(name) |
Есть ли файл |
file(name) |
Один загруженный файл |
files() |
Все загруженные файлы |
files(name) |
Все файлы с указанным именем поля |
bearer_token() |
Bearer-токен из Authorization |
basic_auth() |
Данные Basic Auth |
content_type() |
Content-Type |
user_agent() |
User-Agent |
ip() |
IP клиента |
port() |
Порт клиента |
http_version() |
Версия HTTP |
Response описывает ответ сервера.
return Response(200, "OK", "text/plain; charset=utf-8");Обычно удобнее использовать вспомогательные функции:
| Helper | Назначение |
|---|---|
text(body, status = 200) |
Текстовый ответ |
html(body, status = 200) |
HTML ответ |
json(body, status = 200) |
JSON ответ |
redirect(url, status = 302) |
Redirect с заголовком Location |
status(code) |
Пустой ответ с указанным статусом |
file(path, status = 200) |
Ответ с файлом |
error(status) |
JSON ошибка по статусу |
error(status, message) |
JSON ошибка с текстом |
Примеры:
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 принимает аргументы:
set_cookie(name, value, path, max_age, http_only, secure, same_site)Значения по умолчанию:
path = "/"max_age = -1http_only = truesecure = falsesame_site = "Lax"
InAPI использует nlohmann::json.
app.post("/echo", [](Request request) {
Json body = request.json();
return json({
{"you_sent", body}
});
});Если тело запроса содержит невалидный JSON, request.json() бросит nlohmann::json::parse_error.
Для автоматического ответа 422 используйте request.body(schema).
Параметры пути пишутся в фигурных скобках:
app.get("/users/{id}", [](Request request) {
return text(request.param("id"));
});Поддерживаемые типы:
| Синтаксис | Что принимает |
|---|---|
{name} |
Любой текст до / |
{name:int} |
Целое число, включая отрицательное |
{name:path} |
Остаток пути, включая / |
Примеры:
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 понимает такие значения:
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-токен:
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}
});
});Поля формы:
app.post("/form", [](Request request) {
std::string name = request.form_or("name", "anonymous");
return json({
{"name", name}
});
});Один файл:
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()}
});
});Несколько файлов:
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 содержит:
| Поле или метод | Назначение |
|---|---|
name |
Имя поля формы |
filename |
Имя файла |
content_type |
MIME-тип |
content |
Содержимое файла |
headers |
Заголовки части формы |
size() |
Размер содержимого |
empty() |
Проверяет, пустой ли файл |
save(path) |
Сохраняет файл |
Middleware выполняется перед обработчиком маршрута. Он может:
- передать запрос дальше через
next(request); - сразу вернуть ответ;
- изменить ответ после выполнения обработчика.
app.middleware([](Request request, Next next) {
Response response = next(request);
response.header("X-Powered-By", "InAPI");
return response;
});Проверка заголовка:
app.middleware([](Request request, Next next) {
if (request.header("X-API-Key") != "secret") {
return error(401, "Invalid API key");
}
return next(request);
});Порядок выполнения:
- Глобальные middleware приложения.
- Middleware роутера, если маршрут подключен через
include. - Обработчик маршрута.
Router помогает разделять API на группы маршрутов.
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);Итоговые пути:
GET /usersGET /users/{id:int}
У роутера могут быть свои 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);Самый короткий вариант:
app.Cors();Значения по умолчанию:
- origins:
* - methods:
GET,POST,PUT,PATCH,DELETE,OPTIONS - headers:
Content-Type,Authorization
Настройка:
app.Cors(CorsOptions(
{"https://example.com"},
{"GET", "POST"},
{"Content-Type", "Authorization", "X-API-Key"}
));InAPI сам отвечает на preflight OPTIONS запросы.
app.BearerAuth("secret-token");После этого запрос должен передавать заголовок:
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);Для Basic Auth используйте middleware require_auth и вспомогательную функцию basic_auth.
app.middleware(require_auth(
basic_auth("admin", "password"),
"Basic"
));return unauthorized();
return unauthorized("Login required", "Bearer");
return forbidden();
return forbidden("Access denied");Валидация строится через ValidationSchema и 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);
});Если валидация не прошла, InAPI вернет 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"]}
});
});| Правило | Назначение |
|---|---|
string() |
Строка |
integer() |
Целое число |
number() |
Число |
boolean() |
Boolean |
array() |
Массив |
array(field(...)) |
Массив элементов по правилу |
object({...}) |
Объект с вложенными полями |
required() |
Поле обязательно |
optional() |
Поле необязательно |
nullable() |
Разрешает null |
default_value(value) |
Значение по умолчанию |
min(value) |
Минимальное число |
max(value) |
Максимальное число |
min_len(value) |
Минимальная длина строки или массива |
max_len(value) |
Максимальная длина строки или массива |
one_of({...}) |
Одно из разрешенных значений |
regex(pattern) |
Проверка регулярным выражением |
email() |
|
url() |
URL |
uuid() |
UUID |
custom(message, callback) |
Пользовательская проверка |
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);
});Если для статуса нет своего обработчика, InAPI вернет стандартный JSON:
{
"error": "Not found"
}Стандартные сообщения есть для статусов:
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 обрабатывается отдельно и превращается в ответ 422.
app.mount("/static", "public");После этого файлы из папки public доступны по пути /static.
Поведение:
- если запрошена директория, InAPI ищет
index.html; - если файл не найден, InAPI пробует отдать
index.htmlиз корня папки, что удобно для SPA; - путь защищен от выхода за пределы папки через
..; - для статических файлов выставляются
Cache-Control,ETagиLast-Modified; If-None-MatchиIf-Modified-Sinceподдерживаются с ответом304.
InAPI может сгенерировать OpenAPI документ и Swagger UI.
Swagger swagger(
true,
"/docs",
"Users API",
"1.0.0",
"Example InAPI documentation"
);
app.run("0.0.0.0", 8080, Config(), swagger);После запуска доступны:
- Swagger UI:
/docs - OpenAPI JSON:
/docs/openapi.json
Методы маршрутов возвращают RouteDoc, поэтому описание можно добавлять цепочкой:
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");Тип может описать OpenAPI схему через статические методы:
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"}}
};
}
};Использование:
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");Если openapi_name() не указан, имя берется из типа. Если openapi_schema() не указан, схема будет такой:
{
"type": "object"
}Если включить app.BearerAuth(...), Swagger автоматически получит схему BearerAuth.
Для отдельного маршрута:
app.get("/private", []() {
return json({{"private", true}});
})
.bearer_auth()
.summary("Private route");Bearer auth можно включить и на уровне Swagger:
Swagger swagger(true, "/docs", "API", "1.0.0", "", true);Config настраивает сервер.
Config config(
true,
8,
"10mb",
5,
10,
30
);Параметры:
| Параметр | Значение |
|---|---|
logger |
Включает логирование |
threads |
Количество потоков |
max_body_size |
Максимальный размер тела запроса |
read_timeout_seconds |
Таймаут чтения |
write_timeout_seconds |
Таймаут записи |
idle_timeout_seconds |
Таймаут простоя Keep-Alive соединения |
ssl |
SSL настройки |
Значения по умолчанию:
Config(
true,
auto_threads(),
"1mb",
5,
10,
30,
std::nullopt
);Поддерживаемые значения max_body_size:
512kb1mb10mb1gb
Если указать другое значение, будет использовано 1mb.
Варианты запуска:
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 настраивается через SSL внутри Config.
Config config(
true,
8,
"10mb",
5,
10,
30,
SSL("cert.pem", "key.pem")
);
app.run("0.0.0.0", 443, config);Важно:
- для SSL библиотеку нужно собрать с
CPPHTTPLIB_OPENSSL_SUPPORT; - также нужны библиотеки OpenSSL;
- если сертификат или ключ не найдены, InAPI бросит исключение.
Логирование включено по умолчанию.
InAPI пишет:
- сообщение при старте сервера;
- строку на каждый запрос;
- HTTP статус цветом, если терминал поддерживает ANSI цвета.
Отключение:
Config config(false);
app.run("0.0.0.0", 8080, config);Ручное сообщение:
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);