Manual de C++ con Boost para REST API

Desarrollo de un sistema de clínica con JWT, Bcrypt, RethinkDB y Valkey

1. Introducción

Este manual detalla cómo crear una REST API en C++ utilizando Boost para gestionar una clínica médica. La API incluirá:

  • Autenticación con JWT (JSON Web Tokens)
  • Hash de contraseñas con Bcrypt
  • Persistencia de datos en RethinkDB
  • Caché de rutas con Valkey (fork moderno de Redis)
  • Operaciones CRUD para pacientes, doctores y citas

Nota: Este manual asume que tienes conocimientos intermedios de C++ y familiaridad con conceptos de desarrollo web.

2. Configuración del proyecto

Primero, necesitamos configurar nuestro entorno de desarrollo:

Requisitos

  • C++17 o superior
  • Boost 1.75 o superior
  • RethinkDB 2.4 o superior
  • Valkey (o Redis) 7.0 o superior
  • CMake 3.15 o superior

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(ClinicRestAPI VERSION 1.0.0 LANGUAGES CXX)

# Configuración estándar de C++
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Buscar paquetes requeridos
find_package(Boost 1.75.0 REQUIRED COMPONENTS 
    beast 
    system 
    json 
    random 
    url 
    nowide 
    program_options)

# Configuración de dependencias
find_package(RethinkDB REQUIRED)
find_package(Valkey REQUIRED)
find_package(OpenSSL REQUIRED)

# Ejecutable principal
add_executable(clinic_api
    src/main.cpp
    src/server.cpp
    src/database.cpp
    src/auth.cpp
    src/cache.cpp
    src/models.cpp
    src/routes.cpp
)

# Vincular dependencias
target_link_libraries(clinic_api
    PRIVATE
    Boost::beast
    Boost::system
    Boost::json
    Boost::random
    Boost::url
    Boost::nowide
    Boost::program_options
    RethinkDB::RethinkDB
    Valkey::Valkey
    OpenSSL::SSL
    OpenSSL::Crypto
)

3. Estructura del proyecto

Organizaremos el proyecto de la siguiente manera:

clinic_api/
├── CMakeLists.txt
├── include/
│   ├── server.hpp
│   ├── database.hpp
│   ├── auth.hpp
│   ├── cache.hpp
│   ├── models.hpp
│   └── routes.hpp
├── src/
│   ├── main.cpp
│   ├── server.cpp
│   ├── database.cpp
│   ├── auth.cpp
│   ├── cache.cpp
│   ├── models.cpp
│   └── routes.cpp
├── tests/
└── config/
    ├── config.json
    └── jwt_keys/

Archivos principales

Archivo Propósito
server.hpp/cpp Configuración del servidor HTTP con Boost.Beast
database.hpp/cpp Conexión y operaciones con RethinkDB
auth.hpp/cpp Autenticación con JWT y hash de contraseñas
cache.hpp/cpp Integración con Valkey para caché
models.hpp/cpp Modelos de datos (Paciente, Doctor, Cita)
routes.hpp/cpp Definición de rutas y handlers

4. Boost.Beast para HTTP

Boost.Beast es una biblioteca para implementar protocolos web (HTTP/WebSocket) sobre Boost.Asio.

Configuración básica del servidor

// server.hpp
#pragma once

#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <memory>
#include <string>

namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;

class HTTPServer {
public:
    HTTPServer(std::string address, uint16_t port);
    ~HTTPServer();

    void run();
    void stop();

private:
    void start_accept();
    void handle_accept(beast::error_code ec, tcp::socket socket);

    net::io_context ioc_;
    tcp::acceptor acceptor_;
    std::atomic<bool> stopped_{false};
};

Implementación del servidor

// server.cpp
#include "server.hpp"
#include <iostream>

HTTPServer::HTTPServer(std::string address, uint16_t port)
    : ioc_(1), acceptor_(ioc_) {
    
    beast::error_code ec;
    tcp::endpoint endpoint(net::ip::make_address(address), port);
    
    // Abrir el acceptor
    acceptor_.open(endpoint.protocol(), ec);
    if (ec) {
        throw beast::system_error{ec};
    }

    // Permitir reutilizar la dirección
    acceptor_.set_option(net::socket_base::reuse_address(true), ec);
    if (ec) {
        throw beast::system_error{ec};
    }

    // Vincular a la dirección y puerto
    acceptor_.bind(endpoint, ec);
    if (ec) {
        throw beast::system_error{ec};
    }

    // Empezar a escuchar
    acceptor_.listen(net::socket_base::max_listen_connections, ec);
    if (ec) {
        throw beast::system_error{ec};
    }
}

void HTTPServer::run() {
    start_accept();
    ioc_.run();
}

void HTTPServer::start_accept() {
    acceptor_.async_accept(
        [this](beast::error_code ec, tcp::socket socket) {
            handle_accept(ec, std::move(socket));
        });
}

void HTTPServer::handle_accept(beast::error_code ec, tcp::socket socket) {
    if (ec) {
        std::cerr << "Error en accept: " << ec.message() << "\n";
    } else {
        // Crear una nueva sesión para manejar la conexión
        // (implementaremos esto más adelante)
        // std::make_shared<Session>(std::move(socket))->start();
    }

    if (!stopped_) {
        start_accept();
    }
}

void HTTPServer::stop() {
    stopped_ = true;
    ioc_.stop();
}

5. Implementación de JWT

JSON Web Tokens (JWT) nos permitirá manejar autenticación de manera segura y escalable.

Generación y verificación de tokens

// auth.hpp
#pragma once

#include <boost/json.hpp>
#include <string>
#include <chrono>

namespace json = boost::json;

class JWT {
public:
    JWT(std::string secret_key, std::string algorithm = "HS256");
    
    std::string generate(const json::object& payload, 
                         std::chrono::seconds expires_in = std::chrono::hours(1));
    
    bool verify(const std::string& token);
    json::value decode(const std::string& token);

private:
    std::string sign(const std::string& header, const std::string& payload);
    std::string secret_key_;
    std::string algorithm_;
};

Implementación de JWT

// auth.cpp - Implementación parcial
#include "auth.hpp"
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <boost/algorithm/string.hpp>
#include <boost/beast/core/detail/base64.hpp>

std::string JWT::generate(const json::object& payload, 
                          std::chrono::seconds expires_in) {
    
    // Crear el header
    json::object header;
    header["alg"] = algorithm_;
    header["typ"] = "JWT";

    // Crear una copia del payload y añadir timestamps
    json::object payload_copy = payload;
    auto now = std::chrono::system_clock::now();
    auto exp = now + expires_in;
    
    payload_copy["iat"] = std::chrono::duration_cast<std::chrono::seconds>(
        now.time_since_epoch()).count();
    payload_copy["exp"] = std::chrono::duration_cast<std::chrono::seconds>(
        exp.time_since_epoch()).count();

    // Codificar header y payload
    std::string header_str = json::serialize(header);
    std::string payload_str = json::serialize(payload_copy);

    std::string encoded_header;
    std::string encoded_payload;
    
    // Codificación Base64URL
    size_t encode_len = beast::detail::base64::encoded_size(header_str.size());
    encoded_header.resize(encode_len);
    beast::detail::base64::encode(encoded_header.data(), header_str.data(), header_str.size());
    
    encode_len = beast::detail::base64::encoded_size(payload_str.size());
    encoded_payload.resize(encode_len);
    beast::detail::base64::encode(encoded_payload.data(), payload_str.data(), payload_str.size());

    // Eliminar padding y reemplazar caracteres para Base64URL
    boost::algorithm::erase_all(encoded_header, "=");
    boost::algorithm::replace_all(encoded_header, "+", "-");
    boost::algorithm::replace_all(encoded_header, "/", "_");
    
    boost::algorithm::erase_all(encoded_payload, "=");
    boost::algorithm::replace_all(encoded_payload, "+", "-");
    boost::algorithm::replace_all(encoded_payload, "/", "_");

    // Crear la firma
    std::string signature = sign(encoded_header, encoded_payload);

    // Construir el token JWT
    return encoded_header + "." + encoded_payload + "." + signature;
}

bool JWT::verify(const std::string& token) {
    // 1. Dividir el token en sus partes
    std::vector<std::string> parts;
    boost::algorithm::split(parts, token, boost::is_any_of("."));
    
    if (parts.size() != 3) {
        return false;
    }

    // 2. Verificar la firma
    std::string signature = sign(parts[0], parts[1]);
    
    // Comparar firmas (a prueba de timing attacks)
    bool sig_match = boost::algorithm::equals(signature, parts[2]);
    
    if (!sig_match) {
        return false;
    }

    // 3. Verificar expiración
    auto payload = decode(token);
    auto& obj = payload.as_object();
    
    if (!obj.contains("exp")) {
        return false;
    }

    auto now = std::chrono::system_clock::now();
    auto exp_time = std::chrono::system_clock::time_point(
        std::chrono::seconds(obj["exp"].as_int64()));
    
    return now < exp_time;
}

6. Hash de contraseñas con Bcrypt

Para almacenar contraseñas de manera segura, usaremos el algoritmo bcrypt.

Implementación de Bcrypt

// auth.hpp - Añadir esta clase
class PasswordHasher {
public:
    PasswordHasher(int work_factor = 12);
    
    std::string hash(const std::string& password);
    bool verify(const std::string& password, const std::string& hash);

private:
    int work_factor_;
};

Uso de OpenSSL para Bcrypt

// auth.cpp - Implementación de PasswordHasher
#include <openssl/evp.h>
#include <openssl/rand.h>

std::string PasswordHasher::hash(const std::string& password) {
    // Generar salt
    unsigned char salt[16];
    if (RAND_bytes(salt, sizeof(salt)) != 1) {
        throw std::runtime_error("Error generando salt");
    }

    // Generar hash bcrypt
    char bcrypt_hash[61]; // 60 caracteres + null terminator
    if (!bcrypt_hashpw(password.c_str(), salt, bcrypt_hash)) {
        throw std::runtime_error("Error generando hash bcrypt");
    }

    return std::string(bcrypt_hash);
}

bool PasswordHasher::verify(const std::string& password, const std::string& hash) {
    // Verificar contraseña con bcrypt
    return bcrypt_checkpw(password.c_str(), hash.c_str()) == 0;
}

Importante: Nunca almacenes contraseñas en texto plano. Siempre usa un algoritmo de hash seguro como bcrypt que incluye salt y es computacionalmente costoso.

7. Conexión con RethinkDB

RethinkDB es una base de datos NoSQL con capacidades de cambio en tiempo real.

Conexión y manejo básico

// database.hpp
#pragma once

#include <rethinkdb.h>
#include <boost/json.hpp>
#include <string>
#include <memory>

namespace json = boost::json;

class Database {
public:
    Database(const std::string& host, int port, 
              const std::string& db_name);
    
    // Operaciones CRUD para pacientes
    json::value create_patient(const json::object& patient_data);
    json::value get_patient(const std::string& patient_id);
    json::value update_patient(const std::string& patient_id, 
                            const json::object& update_data);
    bool delete_patient(const std::string& patient_id);
    
    // Operaciones similares para doctores y citas...

private:
    std::shared_ptr<rethinkdb::connection> conn_;
    std::string db_name_;
    
    void ensure_tables();
};

Implementación de la conexión

// database.cpp
#include "database.hpp"
#include <stdexcept>

Database::Database(const std::string& host, int port, 
                   const std::string& db_name)
    : db_name_(db_name) {
    
    // Crear opciones de conexión
    rethinkdb::connection::options opts;
    opts.host = host;
    opts.port = port;
    
    // Establecer conexión
    conn_ = std::make_shared<rethinkdb::connection>(opts);
    
    try {
        // Verificar si la base de datos existe, si no, crearla
        auto cursor = rethinkdb::db_list().run(*conn_);
        bool db_exists = false;
        
        for (const auto& db : cursor) {
            if (db.as_string() == db_name) {
                db_exists = true;
                break;
            }
        }
        
        if (!db_exists) {
            rethinkdb::db_create(db_name).run(*conn_);
        }
        
        // Asegurar que las tablas existan
        ensure_tables();
    } catch (const std::exception& e) {
        throw std::runtime_error("Error inicializando base de datos: " + std::string(e.what()));
    }
}

void Database::ensure_tables() {
    const std::vector<std::string> tables = {
        "patients", "doctors", "appointments", "users"
    };
    
    auto cursor = rethinkdb::db(db_name_).table_list().run(*conn_);
    std::unordered_set<std::string> existing_tables;
    
    for (const auto& table : cursor) {
        existing_tables.insert(table.as_string());
    }
    
    for (const auto& table : tables) {
        if (existing_tables.find(table) == existing_tables.end()) {
            rethinkdb::db(db_name_).table_create(table).run(*conn_);
            
            // Crear índices para búsquedas comunes
            if (table == "patients") {
                rethinkdb::db(db_name_).table(table)
                    .index_create("email").run(*conn_);
            } else if (table == "appointments") {
                rethinkdb::db(db_name_).table(table)
                    .index_create("patient_id").run(*conn_);
                rethinkdb::db(db_name_).table(table)
                    .index_create("doctor_id").run(*conn_);
                rethinkdb::db(db_name_).table(table)
                    .index_create("date").run(*conn_);
            }
        }
    }
}

// Ejemplo de implementación de CRUD para pacientes
json::value Database::create_patient(const json::object& patient_data) {
    auto result = rethinkdb::db(db_name_)
        .table("patients")
        .insert(patient_data)
        .run(*conn_);
    
    return json::parse(result.to_json());
}

json::value Database::get_patient(const std::string& patient_id) {
    auto cursor = rethinkdb::db(db_name_)
        .table("patients")
        .get(patient_id)
        .run(*conn_);
    
    if (cursor.is_empty()) {
        throw std::runtime_error("Paciente no encontrado");
    }
    
    return json::parse(cursor.to_json());
}

8. Cache con Valkey (Redis)

Valkey es un fork moderno de Redis que usaremos para cachear respuestas y mejorar el rendimiento.

Cliente Valkey

// cache.hpp
#pragma once

#include <valkey.h>
#include <boost/json.hpp>
#include <string>
#include <memory>

namespace json = boost::json;

class Cache {
public:
    Cache(const std::string& host, int port);
    
    // Operaciones básicas
    void set(const std::string& key, const std::string& value, 
              std::chrono::seconds ttl = std::chrono::seconds(0));
    std::string get(const std::string& key);
    bool del(const std::string& key);
    bool exists(const std::string& key);
    
    // Operaciones específicas para la API
    void cache_response(const std::string& route, const json::value& response,
                       std::chrono::seconds ttl = std::chrono::minutes(5));
    json::value get_cached_response(const std::string& route);

private:
    std::unique_ptr<valkey::client> client_;
};

Implementación del caché

// cache.cpp
#include "cache.hpp"
#include <stdexcept>

Cache::Cache(const std::string& host, int port) {
    valkey::connection_options opts;
    opts.host = host;
    opts.port = port;
    
    try {
        client_ = std::make_unique<valkey::client>(opts);
        // Verificar conexión
        client_->ping();
    } catch (const std::exception& e) {
        throw std::runtime_error("Error conectando a Valkey: " + std::string(e.what()));
    }
}

void Cache::set(const std::string& key, const std::string& value,
               std::chrono::seconds ttl) {
    if (ttl.count() > 0) {
        client_->setex(key, static_cast<long long>(ttl.count()), value);
    } else {
        client_->set(key, value);
    }
}

std::string Cache::get(const std::string& key) {
    auto reply = client_->get(key);
    if (reply.is_null()) {
        return "";
    }
    return reply.as_string();
}

void Cache::cache_response(const std::string& route, const json::value& response,
                           std::chrono::seconds ttl) {
    std::string key = "route:" + route;
    std::string value = json::serialize(response);
    set(key, value, ttl);
}

json::value Cache::get_cached_response(const std::string& route) {
    std::string key = "route:" + route;
    std::string value = get(key);
    
    if (value.empty()) {
        throw std::runtime_error("Respuesta no encontrada en caché");
    }
    
    return json::parse(value);
}

Middleware de caché

Podemos crear un middleware que verifique el caché antes de procesar una solicitud:

// server.cpp - Añadir esta función
template <typename Handler>
auto make_cached_handler(Cache& cache, Handler handler) {
    return [&cache, handler](const http::request<http::string_body>& req) {
        std::string cache_key = std::string(req.method_string()) + 
                               ":" + std::string(req.target());
        
        try {
            // Intentar obtener del caché
            auto cached_response = cache.get_cached_response(cache_key);
            http::response<http::string_body> res{http::status::ok, req.version()};
            res.set(http::field::content_type, "application/json");
            res.body() = json::serialize(cached_response);
            res.prepare_payload();
            return res;
        } catch (const std::exception&) {
            // No está en caché, procesar normalmente
            auto response = handler(req);
            
            // Almacenar en caché si fue exitoso
            if (response.result() == http::status::ok) {
                auto body = json::parse(response.body());
                cache.cache_response(cache_key, body);
            }
            
            return response;
        }
    };
}

9. Implementación del CRUD

Implementaremos las operaciones CRUD para pacientes, doctores y citas.

Modelos de datos

// models.hpp
#pragma once

#include <boost/json.hpp>
#include <string>
#include <chrono>

namespace json = boost::json;

struct Patient {
    std::string id;
    std::string name;
    std::string email;
    std::string phone;
    std::string address;
    std::string birth_date;
    std::string blood_type;
    
    Patient() = default;
    Patient(const json::object& obj);
    
    json::object to_json() const;
};

struct Doctor {
    std::string id;
    std::string name;
    std::string email;
    std::string phone;
    std::string specialization;
    std::string license_number;
    
    Doctor() = default;
    Doctor(const json::object& obj);
    
    json::object to_json() const;
};

struct Appointment {
    std::string id;
    std::string patient_id;
    std::string doctor_id;
    std::string date;
    std::string time;
    std::string status; // scheduled, completed, canceled
    std::string notes;
    
    Appointment() = default;
    Appointment(const json::object& obj);
    
    json::object to_json() const;
};

Implementación de los modelos

// models.cpp
#include "models.hpp"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>

Patient::Patient(const json::object& obj) {
    if (obj.contains("id")) {
        id = obj.at("id").as_string().c_str();
    } else {
        // Generar un nuevo UUID si no se proporciona
        id = boost::uuids::to_string(boost::uuids::random_generator()());
    }
    
    name = obj.at("name").as_string().c_str();
    email = obj.at("email").as_string().c_str();
    phone = obj.at("phone").as_string().c_str();
    address = obj.at("address").as_string().c_str();
    birth_date = obj.at("birth_date").as_string().c_str();
    blood_type = obj.at("blood_type").as_string().c_str();
}

json::object Patient::to_json() const {
    return {
        {"id", id},
        {"name", name},
        {"email", email},
        {"phone", phone},
        {"address", address},
        {"birth_date", birth_date},
        {"blood_type", blood_type}
    };
}

// Implementaciones similares para Doctor y Appointment...

Handlers para el CRUD

// routes.cpp - Ejemplo de handlers
#include "routes.hpp"
#include "models.hpp"
#include "auth.hpp"
#include "database.hpp"
#include <boost/beast/http.hpp>

namespace http = boost::beast::http;

http::response<http::string_body> 
handle_create_patient(const http::request<http::string_body>& req, 
                     Database& db, JWT& jwt) {
    
    // Verificar autenticación
    auto auth_header = req.find(http::field::authorization);
    if (auth_header == req.end()) {
        return create_unauthorized_response(req, "Falta token de autenticación");
    }
    
    std::string token = std::string(auth_header->value());
    if (!jwt.verify(token)) {
        return create_unauthorized_response(req, "Token inválido o expirado");
    }
    
    // Parsear cuerpo de la solicitud
    auto patient_data = json::parse(req.body()).as_object();
    Patient patient(patient_data);
    
    // Insertar en la base de datos
    auto result = db.create_patient(patient.to_json());
    
    // Construir respuesta
    http::response<http::string_body> res{http::status::created, req.version()};
    res.set(http::field::content_type, "application/json");
    res.body() = json::serialize(result);
    res.prepare_payload();
    
    return res;
}

http::response<http::string_body> 
handle_get_patient(const http::request<http::string_body>& req, 
                  Database& db, JWT& jwt, Cache& cache) {
    
    // Extraer ID del paciente de la URL
    std::string patient_id = std::string(req.target()).substr(10); // "/patients/123"
    
    try {
        // Obtener paciente de la base de datos
        auto patient = db.get_patient(patient_id);
        
        // Construir respuesta
        http::response<http::string_body> res{http::status::ok, req.version()};
        res.set(http::field::content_type, "application/json");
        res.body() = json::serialize(patient);
        res.prepare_payload();
        
        return res;
    } catch (const std::exception& e) {
        return create_error_response(req, http::status::not_found, e.what());
    }
}

// Funciones similares para update_patient, delete_patient, y operaciones para doctores y citas...

10. Definición de rutas

Definiremos las rutas de nuestra API y las conectaremos con los handlers.

Tabla de rutas

Método Ruta Descripción
POST /api/auth/register Registrar un nuevo usuario
POST /api/auth/login Iniciar sesión y obtener JWT
GET /api/patients Listar pacientes (paginated)
POST /api/patients Crear un nuevo paciente
GET /api/patients/{id} Obtener un paciente específico
PUT /api/patients/{id} Actualizar un paciente
DELETE /api/patients/{id} Eliminar un paciente
GET /api/doctors Listar doctores
GET /api/appointments Listar citas (con filtros)
POST /api/appointments Crear una nueva cita

Router básico

// routes.hpp
#pragma once

#include <boost/beast/http.hpp>
#include <functional>
#include <unordered_map>
#include <memory>

namespace http = boost::beast::http;

using Request = http::request<http::string_body>;
using Response = http::response<http::string_body>;
using Handler = std::function<Response(const Request&)>;

class Router {
public:
    Router() = default;
    
    void add_route(http::verb method, const std::string& path, Handler handler);
    Response handle_request(const Request& req);

private:
    std::unordered_map<std::string, 
        std::unordered_map<http::verb, Handler>> routes_;
};

Implementación del router

// routes.cpp
#include "routes.hpp"
#include <boost/algorithm/string.hpp>

void Router::add_route(http::verb method, const std::string& path, Handler handler) {
    routes_[path][method] = std::move(handler);
}

Response Router::handle_request(const Request& req) {
    std::string path = std::string(req.target());
    
    // Buscar coincidencia exacta primero
    if (routes_.find(path) != routes_.end()) {
        auto& methods = routes_.at(path);
        if (methods.find(req.method()) != methods.end()) {
            return methods.at(req.method())(req);
        }
    }
    
    // Buscar rutas con parámetros (como /patients/:id)
    for (const auto& [route_path, methods] : routes_) {
        if (route_path.find(":") != std::string::npos) {
            std::vector<std::string> route_parts;
            boost::algorithm::split(route_parts, route_path, boost::is_any_of("/"));
            
            std::vector<std::string> path_parts;
            boost::algorithm::split(path_parts, path, boost::is_any_of("/"));
            
            if (route_parts.size() == path_parts.size()) {
                bool match = true;
                for (size_t i = 0; i < route_parts.size(); ++i) {
                    if (route_parts[i] != path_parts[i] && !route_parts[i].starts_with(":")) {
                        match = false;
                        break;
                    }
                }
                
                if (match && methods.find(req.method()) != methods.end()) {
                    return methods.at(req.method())(req);
                }
            }
        }
    }
    
    // No se encontró la ruta
    return create_error_response(req, http::status::not_found, "Ruta no encontrada");
}

Configuración de rutas en main.cpp

// main.cpp - Configuración de rutas
#include "server.hpp"
#include "database.hpp"
#include "auth.hpp"
#include "cache.hpp"
#include "routes.hpp"
#include <boost/program_options.hpp>

int main(int argc, char* argv[]) {
    // Configuración inicial...
    
    // Inicializar componentes
    Database db(config.db_host, config.db_port, config.db_name);
    Cache cache(config.redis_host, config.redis_port);
    JWT jwt(config.jwt_secret);
    PasswordHasher hasher();
    
    // Configurar rutas
    Router router;
    
    // Autenticación
    router.add_route(http::verb::post, "/api/auth/register", 
        [&](const auto& req) { return handle_register(req, db, hasher, jwt); });
    
    router.add_route(http::verb::post, "/api/auth/login", 
        [&](const auto& req) { return handle_login(req, db, hasher, jwt); });
    
    // Pacientes
    router.add_route(http::verb::get, "/api/patients", 
        make_cached_handler(cache, 
            [&](const auto& req) { return handle_list_patients(req, db, jwt); }));
    
    router.add_route(http::verb::post, "/api/patients", 
        [&](const auto& req) { return handle_create_patient(req, db, jwt); });
    
    router.add_route(http::verb::get, "/api/patients/:id", 
        make_cached_handler(cache, 
            [&](const auto& req) { return handle_get_patient(req, db, jwt, cache); }));
    
    // Iniciar servidor
    HTTPServer server(config.address, config.port);
    server.set_request_handler([&router](const auto& req) {
        return router.handle_request(req);
    });
    
    server.run();
    
    return 0;
}

11. Consideraciones de seguridad

Implementaremos varias medidas de seguridad para proteger nuestra API.

Protecciones básicas

  • HTTPS: Siempre usar HTTPS en producción.
  • CORS: Configurar adecuadamente los headers CORS.
  • Rate limiting: Limitar peticiones para prevenir ataques de fuerza bruta.
  • Validación de entrada: Validar todos los datos de entrada.

Middleware de seguridad

// server.cpp - Middleware de seguridad
http::response<http::string_body> 
add_security_headers(http::response<http::string_body> res) {
    // Headers de seguridad básicos
    res.set(http::field::strict_transport_security, "max-age=63072000; includeSubDomains; preload");
    res.set(http::field::x_content_type_options, "nosniff");
    res.set(http::field::x_frame_options, "DENY");
    res.set(http::field::x_xss_protection, "1; mode=block");
    res.set(http::field::content_security_policy, 
        "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'");
    
    // Headers CORS (ajustar según necesidades)
    res.set(http::field::access_control_allow_origin, "*");
    res.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE, OPTIONS");
    res.set(http::field::access_control_allow_headers, 
        "Content-Type, Authorization");
    
    return res;
}

Validación de entrada

// auth.cpp - Validación de registro
void validate_registration(const json::object& data) {
    static const std::regex email_regex(
        R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)");
    
    if (!data.contains("email") || !data.contains("password")) {
        throw std::invalid_argument("Email y password son requeridos");
    }
    
    std::string email = data.at("email").as_string().c_str();
    std::string password = data.at("password").as_string().c_str();
    
    if (!std::regex_match(email, email_regex)) {
        throw std::invalid_argument("Email inválido");
    }
    
    if (password.length() < 8) {
        throw std::invalid_argument("Password debe tener al menos 8 caracteres");
    }
}

12. Despliegue

Finalmente, veremos cómo desplegar nuestra aplicación en producción.

Configuración de producción

Crear un archivo config/production.json:

{
    "address": "0.0.0.0",
    "port": 8080,
    "db_host": "rethinkdb-prod",
    "db_port": 28015,
    "db_name": "clinic_prod",
    "redis_host": "valkey-prod",
    "redis_port": 6379,
    "jwt_secret": "SECRETO_MUY_SEGURO_AQUI",
    "ssl_cert": "/etc/ssl/certs/clinic_api.crt",
    "ssl_key": "/etc/ssl/private/clinic_api.key"
}

Dockerfile para producción

# Dockerfile
FROM ubuntu:22.04 AS builder

# Instalar dependencias de construcción
RUN apt-get update && apt-get install -y \
    build-essential \
    cmake \
    libboost-all-dev \
    libssl-dev \
    librethinkdb-dev \
    libvalkey-dev \
    git

# Copiar código fuente
COPY . /app
WORKDIR /app/build

# Construir la aplicación
RUN cmake .. && cmake --build . --config Release

# Imagen final más pequeña
FROM ubuntu:22.04

# Instalar solo dependencias de runtime
RUN apt-get update && apt-get install -y \
    libboost-system1.74.0 \
    libboost-json1.74.0 \
    libssl3 \
    librethinkdb2.4 \
    libvalkey7 && \
    rm -rf /var/lib/apt/lists/*

# Copiar binario y configuración
COPY --from=builder /app/build/clinic_api /usr/local/bin/
COPY config/production.json /etc/clinic_api/config.json

# Exponer puerto
EXPOSE 8080

# Comando de inicio
CMD ["clinic_api", "--config", "/etc/clinic_api/config.json"]

docker-compose.yml

# docker-compose.yml
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - rethinkdb
      - valkey
    environment:
      - APP_ENV=production
    restart: unless-stopped

  rethinkdb:
    image: rethinkdb:2.4
    ports:
      - "28015:28015"
      - "8081:8080" # Admin UI
    volumes:
      - rethinkdb_data:/data
    restart: unless-stopped

  valkey:
    image: valkey/valkey:7
    ports:
      - "6379:6379"
    volumes:
      - valkey_data:/data
    restart: unless-stopped

volumes:
  rethinkdb_data:
  valkey_data:

Nota: Para producción real, considera añadir un proxy inverso como Nginx para manejar SSL terminación, balanceo de carga y compresión.