---
title: Hello World in C++
description: Create your first WendyOS application using C++
language: cpp
---

## Creating Your First C++ Application

This guide will walk you through creating your first WendyOS application in C++. The Wendy C++ template scaffolds a small HTTP server built on the [Drogon](https://drogon.org/) framework, so your very first app is a real, long-running service you can reach from a browser. This is a great way to prove you can send an app to your device and verify your development environment is set up correctly.

### Prerequisites

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

<Callout type="info">
  **Containerized Deployment**: C++ applications run inside containers on your WendyOS device. The Dockerfile in this guide is configured for the NVIDIA Jetson Orin Nano's ARM64 architecture using `arm64v8/debian:bookworm-slim` as the base image. The Wendy CLI handles the build and deploy for you — you do not need a local C++ toolchain.
</Callout>

### Creating the Project

<Steps>
  <Step>
    ### Initialize the Project

    Start from the Wendy C++ template:

    ```bash
    wendy init hello-world --target wendyos --language cpp --template simple-api --var APP_ID=hello-world --var PORT=7001 --assistant skip --git-init no
    cd hello-world
    ```

    <CliShot flow="init" step="wizard" alt="wendy init scaffolding the C++ hello-world project" />

    The template creates `CMakeLists.txt`, `main.cpp`, `Dockerfile`, and `wendy.json`. Continue below to understand the generated project.
  </Step>

  <Step>
    ### Run on WendyOS

    ```bash
    wendy run
    ```

    <CliShot flow="run" step="deploy" alt="wendy run building and deploying the C++ hello-world app" />

    <CliClip flow="run" alt="wendy run deploying the C++ hello-world app end to end" />

    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. Because the generated `wendy.json` includes a `postStart` hook, your browser opens automatically once the server is ready and shows the JSON response from `/`.
  </Step>
</Steps>

## Code Breakdown

<Steps>
  <Step>
    ### Generated Application

    The generated `main.cpp` starts a Drogon HTTP server with three routes and listens on port `7001`:

    ```cpp
    #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 << ":7001" << std::endl;

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

        return 0;
    }
    ```

    <Callout type="info">
      **A real server, not a print-and-exit app**: Unlike a classic "hello world" that prints a line and exits, this app calls `app().run()` and stays running so it can serve requests. The default route `GET /` returns `{"message":"hello-world"}`.
    </Callout>
  </Step>

  <Step>
    ### Generated CMakeLists.txt

    The generated `CMakeLists.txt` finds Drogon and builds the server binary:

    ```cmake
    cmake_minimum_required(VERSION 3.16)
    project(hello-world VERSION 0.1.0 LANGUAGES CXX)

    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

    find_package(Drogon REQUIRED)

    add_executable(hello-world main.cpp)

    target_link_libraries(hello-world PRIVATE Drogon::Drogon)
    ```
  </Step>

  <Step>
    ### 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, then builds the app; the runtime stage ships only the binary and its shared-library dependencies:

    ```dockerfile
    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/hello-world /usr/local/bin/hello-world

    EXPOSE 7001

    CMD ["hello-world"]
    ```

    <Callout type="info">
      **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.
    </Callout>
  </Step>

  <Step>
    ### Generated wendy.json

    The generated `wendy.json` already wires up everything the app needs — the `network` entitlement, a readiness probe on port `7001`, and a `postStart` hook that opens your browser when the server is ready:

    ```json
    {
        "appId": "hello-world",
        "platform": "linux",
        "version": "0.1.0",
        "entitlements": [
            {
                "type": "network"
            }
        ],
        "readiness": {
            "tcpSocket": { "port": 7001 },
            "timeoutSeconds": 30
        },
        "hooks": {
            "postStart": {
                "cli": "wendy utils open-browser http://${WENDY_HOSTNAME}:7001"
            }
        }
    }
    ```
  </Step>
</Steps>

## Run Again on WendyOS

Deploy your application to a connected WendyOS device:

```bash
wendy run
```

This will build your application into a container, deploy it to your device, and stream the logs. When the readiness probe passes, the `postStart` hook opens `http://${WENDY_HOSTNAME}:7001` in your browser, where you should see:

```json
{"message":"hello-world"}
```

## Verifying Deployment on Your Device

After deploying your application to a WendyOS device, you can verify it is running by listing the applications on your device:

```bash
wendy device apps list
✔︎ Searching for WendyOS devices [5.3s]
✔︎ Listing applications: True Probe [USB, Ethernet, LAN]
╭───────────────┬─────────┬─────────┬──────────╮
│ App           │ Version │ State   │ Failures │
├───────────────┼─────────┼─────────┼──────────┤
│ hello-world   │ 0.1.0   │ Running │ 0        │
╰───────────────┴─────────┴─────────┴──────────╯
```

<CliShot flow="apps-list" step="apps" alt="wendy device apps list showing the running hello-world app" />

<Callout type="info">
  **Expected State**: Because this app is an HTTP server, it shows as "Running" — it stays up to serve requests rather than exiting. The "Failures" column showing `0` confirms it started cleanly. (To stop it, run `wendy device apps stop hello-world`.)
</Callout>

## Next Steps

Now that you have a basic C++ application running:

- Build out the API in the [Simple Web Server](../../../../guides/tutorials/cpp/simple-web-server/) guide
- Stream a camera feed with the [Camera Feed](../../../../guides/tutorials/cpp/camera/) guide
- Explore environment variables and configuration
- Integrate with WendyOS device features via HTTP APIs

<Callout type="info">
  **Containerized Deployment**: C++ applications on WendyOS run in containers. This provides isolation and portability, and the Wendy CLI handles the local build setup when deploying to your device.
</Callout>

<Accordions>
  <Accordion title="Testing the app on your own machine">
    There are times when you might not have a WendyOS device around. The easiest way to test locally is with Docker, which mirrors how the app is built and run on the device.

    ### Using Docker Locally

    Build and run your application in a container, mapping the port so you can reach it:

    ```bash
    docker build --platform linux/arm64 -t hello-world .
    docker run --platform linux/arm64 -p 7001:7001 hello-world
    ```

    In another terminal, send a request to the default route:

    ```bash
    curl http://localhost:7001
    ```

    You should see:

    ```json
    {"message":"hello-world"}
    ```

    <Callout type="info">
      **Building from source is required**: The app depends on the Drogon framework, which the Dockerfile compiles from source. Building locally without Docker means installing Drogon and its dependencies (libjsoncpp, libssl, uuid, zlib) yourself, so Docker is the recommended path.
    </Callout>
  </Accordion>
</Accordions>
