---
title: Simple Web Server
description: Build a long-running HTTP server on WendyOS using Hummingbird
language: swift
---

## Building a Web Server with Hummingbird

Often times you'll want a long-running server where you can make HTTP 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.

To prove this out, we'll use [Hummingbird](https://hummingbird.codes/), a lightweight and flexible HTTP server framework for Swift. The `simple-api` template generates a ready-to-run Hummingbird server with OpenTelemetry instrumentation wired in.

### Prerequisites

- Wendy CLI installed on your development machine
- Swift 6.2 or later installed **via [swiftly](https://www.swift.org/install/)** (Xcode's Swift is not supported)
- A WendyOS device plugged in over USB or connectable over Wi-Fi

## Setting Up Your Project

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

    Start from the Wendy Hummingbird template:

    ```bash
    wendy init simple-web-server --target wendyos --language swift --template simple-api --var APP_ID=simple-web-server --var PORT=8000 --var SWIFT_VERSION=6.3 --assistant skip --git-init no
    cd simple-web-server
    ```

    <CliShot flow="init" step="wizard" alt="wendy init scaffolding the Swift simple-web-server project" />

    This creates a Swift project with `Package.swift`, a `Sources/` directory, `Dockerfile`, and `wendy.json`.
  </Step>

  <Step>
    ### Run on WendyOS

    ```bash
    wendy run
    ```

    <CliShot flow="run" step="deploy" alt="wendy run deploying the Hummingbird simple-web-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. Because this is a long-running server, the app stays running after deploy.
  </Step>
</Steps>

## Code Breakdown

<Steps>
  <Step>
    ### Generated Package Dependencies

    The generated `Package.swift` at the project root looks like this:

    ```swift
    // swift-tools-version: 6.3

    import PackageDescription

    let package = Package(
        name: "simple-web-server",
        platforms: [
            .macOS(.v14)
        ],
        dependencies: [
            .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.21.1", traits: []),
            .package(url: "https://github.com/swift-otel/swift-otel.git", from: "1.0.0", traits: ["OTLPHTTP", "OTLPGRPC"]),
            .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"),
        ],
        targets: [
            .executableTarget(
                name: "simple-web-server",
                dependencies: [
                    .product(name: "Hummingbird", package: "hummingbird"),
                    .product(name: "OTel", package: "swift-otel"),
                ]
            )
        ]
    )
    ```
  </Step>
</Steps>

Your project layout should look like this:

```
simple-web-server/
├── Package.swift
├── Dockerfile
├── wendy.json
├── .swift-version
└── Sources/
    └── simple-web-server/
        └── App.swift
```

## Generated Web Server

The generated `Sources/simple-web-server/App.swift` contains the server logic. It listens on port `8000` and exposes three routes:

```swift
import Hummingbird
import Logging
import OTel
import ServiceLifecycle

struct Item: Decodable {
    let name: String
    let price: Double
}

struct ItemResponse: ResponseCodable {
    let id: Int
    let name: String
    let price: Double
}

@main
struct SimpleAPI {
    static func main() async throws {
        let observability = try OTel.bootstrap()

        let logger = Logger(label: "simple-web-server")

        let router = Router()
        router.middlewares.add(TracingMiddleware())
        router.middlewares.add(MetricsMiddleware())
        router.middlewares.add(LogRequestsMiddleware(.info))

        router.get("/") { _, _ in
            ["message": "hello-world"]
        }

        router.get("/health") { _, _ -> HTTPResponse.Status in
            .ok
        }

        router.post("/items") { request, context -> ItemResponse in
            let item = try await request.decode(as: Item.self, context: context)
            return ItemResponse(id: 1, name: item.name, price: item.price)
        }

        let app = Application(
            router: router,
            configuration: .init(address: .hostname("0.0.0.0", port: 8000))
        )

        let serviceGroup = ServiceGroup(
            services: [observability, app],
            gracefulShutdownSignals: [.sigterm, .sigint],
            logger: logger
        )

        try await serviceGroup.run()
    }
}
```

`GET /` returns `{"message":"hello-world"}`, `GET /health` returns `200 OK`, and `POST /items` decodes an item from the request body and echoes it back as JSON. The `ServiceGroup` runs both the OpenTelemetry observability service and the HTTP server, and shuts them down gracefully on `SIGTERM`/`SIGINT`.

<Callout type="warning">
  **Important**: It's important that your Hummingbird server is running on `0.0.0.0` and not `localhost` or `127.0.0.1`. `0.0.0.0` binds to all network interfaces on the device, making your server reachable from the host machine and the local network. Using `localhost` or `127.0.0.1` binds only to the loopback interface, so the server would not be accessible from outside the device.
</Callout>

## Running Locally

You can test your server on your development machine before deploying to a WendyOS device:

```bash
swift run
```

Once the build completes, the server starts listening on `0.0.0.0:8000`. You can then test it from another terminal:

```bash
curl http://localhost:8000
# {"message":"hello-world"}

curl http://localhost:8000/health
# (returns HTTP 200)

curl -X POST http://localhost:8000/items \
  -H "Content-Type: application/json" \
  -d '{"name":"Widget","price":9.99}'
# {"id":1,"name":"Widget","price":9.99}
```

Press `Ctrl+C` to stop the local server.

## Generated wendy.json

The generated `wendy.json` requests the `network` entitlement and already includes a readiness probe plus a `postStart` hook that opens your browser when the server is ready — you don't need to add these by hand:

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

## Run Again on WendyOS

When you're ready to deploy, run:

```bash
wendy run
```

Once the readiness probe passes, the `postStart` hook opens `http://${WENDY_HOSTNAME}:8000` in your browser. You can also access it manually with curl:

```bash
curl http://wendyos-device.local:8000
```

Take note of the hostname of your WendyOS device. It will be something like `wendyos-device.local`; `wendy run` will output the hostname of your device.

You should see the response:

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

## Verifying Deployment

You can also verify the server 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 │
├───────────────────┼─────────┼─────────┼──────────┤
│ simple-web-server │ 0.1.0   │ Running │ 0        │
╰───────────────────┴─────────┴─────────┴──────────╯
```

<CliShot flow="apps-list" step="apps" alt="wendy device apps list showing the running simple-web-server app" />

The app shows as **Running** because it is a long-running HTTP server.

## Learn More

Hummingbird is a fantastic lightweight web framework for Swift. Learn more by visiting [https://hummingbird.codes/](https://hummingbird.codes/) and exploring the examples to make your server much more advanced.

## Next Steps

Now that you have a basic web server running:

- Add more routes to handle different endpoints
- Implement POST, PUT, and DELETE routes for a full REST API
- Connect to WendyOS device features to control hardware via HTTP
- Add WebSocket support for real-time communication
- Explore Hummingbird's middleware and authentication features
- Check out [Hummingbird Examples](https://github.com/hummingbird-project/hummingbird-examples) for a plethora of comprehensive examples
