WendyOS Docs
Guides & TutorialsTutorialsC++

Simple Web Server

Build a long-running HTTP server on WendyOS using C++

Building a Web Server with Drogon

Often times you'll want a long-running server where you can make HTTP or WebSocket calls to your WendyOS device. This allows your device to accept incoming requests and respond to them, making it easy to build interactive applications or APIs that can be accessed from other devices on your network.

This guide shows you how to build a web server using Drogon, a high-performance C++14/17 HTTP application framework. The Wendy C++ template scaffolds the server, Dockerfile, and wendy.json for you.

Prerequisites

  • Wendy CLI installed on your development machine
  • A WendyOS device plugged in over USB or connectable over Wi-Fi

Setting Up Your Project

Initialize the Project

Start from the Wendy C++ web server template:

wendy init simple-server --target wendyos --language cpp --template simple-api --var APP_ID=simple-server --var PORT=3000 --assistant skip --git-init no
cd simple-server
wendy init scaffolding the C++ simple-server project

The template creates CMakeLists.txt, main.cpp, Dockerfile, and wendy.json. The sections below explain the generated server and the pieces you can customize.

Run on WendyOS

wendy run
wendy run deploying the C++ Drogon simple-server

Wendy will build the app, ask you to select a device if one is not already configured, deploy the app, and stream the run output. The generated wendy.json includes a postStart hook, so your browser opens automatically once the server is ready.

Code Breakdown

Generated Web Server

The generated main.cpp registers three routes on a Drogon server listening on port 3000:

#include <drogon/drogon.h>
#include <cstdlib>
#include <iostream>
#include <string>

using namespace drogon;

int main() {
    const char* env_hostname = std::getenv("WENDY_HOSTNAME");
    std::string hostname = env_hostname ? env_hostname : "0.0.0.0";

    app().registerHandler(
        "/",
        [](const HttpRequestPtr&, std::function<void(const HttpResponsePtr&)>&& callback) {
            std::cout << "Received request: GET /" << std::endl;
            Json::Value json;
            json["message"] = "hello-world";
            auto resp = HttpResponse::newHttpJsonResponse(json);
            callback(resp);
        },
        {Get});

    app().registerHandler(
        "/health",
        [](const HttpRequestPtr&, std::function<void(const HttpResponsePtr&)>&& callback) {
            Json::Value json;
            json["status"] = "ok";
            auto resp = HttpResponse::newHttpJsonResponse(json);
            callback(resp);
        },
        {Get});

    app().registerHandler(
        "/items",
        [](const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback) {
            std::cout << "Received request: POST /items" << std::endl;
            auto body = req->getJsonObject();
            if (!body) {
                Json::Value err;
                err["error"] = "Invalid JSON";
                auto resp = HttpResponse::newHttpJsonResponse(err);
                resp->setStatusCode(k400BadRequest);
                callback(resp);
                return;
            }

            Json::Value item;
            item["id"] = 1;
            item["name"] = (*body)["name"].asString();
            item["price"] = (*body)["price"].asDouble();
            auto resp = HttpResponse::newHttpJsonResponse(item);
            resp->setStatusCode(k201Created);
            callback(resp);
        },
        {Post});

    std::cout << "Server running on http://" << hostname << ":3000" << std::endl;

    app().addListener("0.0.0.0", 3000);
    app().run();

    return 0;
}

Three routes: This example includes:

  • GET / — returns {"message":"hello-world"}
  • GET /health — returns {"status":"ok"}, handy for health checks
  • POST /items — parses a JSON body (name, price) and echoes it back with an id and a 201 Created status, or returns 400 for invalid JSON

Important: The server binds to 0.0.0.0 rather than localhost or 127.0.0.1. This is because your WendyOS device needs to accept connections from other devices on the network. Binding to 0.0.0.0 makes the server reachable externally.

Generated CMakeLists.txt

The generated CMakeLists.txt finds Drogon and links it into the server binary:

cmake_minimum_required(VERSION 3.16)
project(simple-server VERSION 0.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Drogon REQUIRED)

add_executable(simple-server main.cpp)

target_link_libraries(simple-server PRIVATE Drogon::Drogon)

Generated Dockerfile

The generated project includes a multi-stage Dockerfile configured for the NVIDIA Jetson Orin Nano (ARM64). The build stage compiles Drogon v1.9.12 from source and builds the app; the runtime stage ships only the binary and its shared-library dependencies:

FROM arm64v8/debian:bookworm-slim AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    cmake \
    git \
    ca-certificates \
    libjsoncpp-dev \
    libssl-dev \
    uuid-dev \
    zlib1g-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /deps

RUN git clone --branch v1.9.12 --depth 1 https://github.com/drogonframework/drogon.git && \
    cd drogon && \
    git submodule update --init && \
    cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=OFF -DBUILD_CTL=OFF -DBUILD_ORM=OFF && \
    cmake --build build -j$(nproc) && \
    cmake --install build

WORKDIR /app

COPY CMakeLists.txt main.cpp ./

RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && \
    cmake --build build --config Release

FROM arm64v8/debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    libjsoncpp25 \
    libssl3 \
    libuuid1 \
    zlib1g \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/build/simple-server /usr/local/bin/simple-server

EXPOSE 3000

CMD ["simple-server"]

Building Drogon from source takes a while: The first wendy run compiles Drogon inside the build stage, which can take several minutes. Subsequent builds reuse the cached layer and are much faster.

Generated wendy.json

The generated wendy.json already includes the network entitlement, a readiness probe on port 3000, and a postStart hook that opens your browser once the server is ready — you don't need to add these by hand:

{
    "appId": "simple-server",
    "platform": "linux",
    "version": "0.1.0",
    "entitlements": [
        {
            "type": "network"
        }
    ],
    "readiness": {
        "tcpSocket": { "port": 3000 },
        "timeoutSeconds": 30
    },
    "hooks": {
        "postStart": {
            "cli": "wendy utils open-browser http://${WENDY_HOSTNAME}:3000"
        }
    }
}

Run Again on WendyOS

Deploy your containerized web server to your WendyOS device:

wendy run

The Wendy CLI will build your container, transfer it to your device, and run it with the appropriate port mapping.

Test Your Server on WendyOS

After deploying your server, the postStart hook opens your browser automatically when the readiness probe passes. You can also open your browser manually and navigate to:

http://wendyos-true-probe.local:3000

Replace the hostname: Each WendyOS device has a unique hostname. Replace wendyos-true-probe with your device's actual hostname shown in the CLI output. Don't forget to include the port.

The default route returns:

{"message":"hello-world"}

This confirms your web server is successfully running on your WendyOS device and accepting requests from your network.

Verifying Deployment

You can verify the server container is running by listing the applications on your device:

wendy device apps list
✔︎ Searching for WendyOS devices [5.3s]
✔︎ Listing applications: True Probe [USB, Ethernet, LAN]
╭───────────────┬─────────┬─────────┬──────────╮
 App Version State Failures
├───────────────┼─────────┼─────────┼──────────┤
 simple-server 0.1.0 Running 0
╰───────────────┴─────────┴─────────┴──────────╯
wendy device apps list showing the running simple-server app

Because the server stays up to serve requests, it shows as "Running" with 0 failures.

More Code Notes

Let's break down what the code does:

  1. Include Drogon: <drogon/drogon.h> brings in the framework. JSON support is built in via Json::Value.

  2. Read the hostname: The app reads WENDY_HOSTNAME from the environment so it can log a reachable URL, falling back to 0.0.0.0.

  3. Register routes: app().registerHandler(...) attaches a lambda handler and the allowed HTTP methods ({Get}, {Post}) to each path. POST /items parses the request body with req->getJsonObject() and validates it.

  4. Start the server: app().addListener("0.0.0.0", 3000) binds the listener and app().run() blocks, serving connections on port 3000.

Alternative Frameworks

While the template uses Drogon, other excellent C++ web frameworks are available:

  • Crow: A Flask-inspired micro web framework
  • Pistache: A modern and elegant HTTP and REST framework
  • Oat++: Light and powerful C++ web framework
  • cpp-httplib: A lightweight, single-file HTTP/HTTPS server and client library

All of these frameworks can be containerized and deployed to WendyOS following a similar Docker-based approach.

Next Steps

Now that you have a basic web server running:

  • Add more routes to handle different endpoints
  • Implement PUT and DELETE routes for a full REST API
  • Add request validation and error handling
  • Parse query parameters
  • Connect to WendyOS device features to control hardware via HTTP
  • Implement WebSocket support for real-time communication (see the Camera Feed guide)

On this page