WendyOS Docs
Guides & TutorialsTutorialsSwift

Hello World in Swift

Create your first WendyOS application using Swift

Creating Your First Swift Application

This guide will walk you through creating your first WendyOS application in Swift. This is a great way to prove that you can send an app to your device and verify your development environment is set up correctly.

The simple-api template generates a small, long-running HTTP server built on Hummingbird. It stays running on your device and responds to requests, so you can open it in a browser and see a live response.

Prerequisites

  • Wendy CLI installed on your development machine
  • Swift 6.2 or later installed via swiftly (Xcode's Swift is not supported)
  • A WendyOS device plugged in over USB or connectable over Wi-Fi

Creating the Project

Initialize the Project

Start from the Wendy Swift template:

wendy init hello-world --target wendyos --language swift --template simple-api --var APP_ID=hello-world --var PORT=6001 --var SWIFT_VERSION=6.3 --assistant skip --git-init no
cd hello-world
wendy init scaffolding the Swift hello-world project

This creates a ready-to-run Swift project with Package.swift, Dockerfile, and wendy.json. The next steps explain the generated files.

Run on WendyOS

wendy run
wendy run building and deploying the Swift hello-world appwendy run deploying the Swift 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 this is a long-running server, the app stays running after deploy and the generated postStart hook opens the response in your browser automatically.

Code Breakdown

Explore the Project Structure

After initialization, you'll have the following structure:

hello-world/
├── Package.swift
├── Dockerfile
├── wendy.json
├── .swift-version
└── Sources/
    └── hello-world/
        └── App.swift

The main application code is in Sources/hello-world/App.swift.

Generated Package.swift

The generated Package.swift pins Swift 6.3 and pulls in Hummingbird plus OpenTelemetry support:

// swift-tools-version: 6.3

import PackageDescription

let package = Package(
    name: "hello-world",
    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: "hello-world",
            dependencies: [
                .product(name: "Hummingbird", package: "hummingbird"),
                .product(name: "OTel", package: "swift-otel"),
            ]
        )
    ]
)

Generated Application Code

The generated Sources/hello-world/App.swift starts a Hummingbird server that listens on port 6001 and serves a few routes:

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: "hello-world")

        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: 6001))
        )

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

        try await serviceGroup.run()
    }
}

The @main attribute marks the entry point. GET / returns {"message":"hello-world"}, GET /health returns 200 OK, and POST /items echoes a decoded item back as JSON. The server binds to 0.0.0.0 so it's reachable from your host machine and the local network, and it runs until it receives a shutdown signal.

Generated wendy.json

The generated wendy.json requests the network entitlement, adds a readiness probe on port 6001, and registers a postStart hook that opens your browser once the server is ready:

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

Generated Dockerfile

The generated Dockerfile is a multi-stage build: it compiles a release binary with swift:6.3-bookworm and runs it on the smaller swift:6.3-bookworm-slim image:

FROM swift:6.3-bookworm AS builder

WORKDIR /app
COPY Package.swift ./
COPY Sources Sources
RUN swift build -c release

FROM swift:6.3-bookworm-slim

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

WORKDIR /app
COPY --from=builder /app/.build/release/hello-world /usr/local/bin/hello-world

EXPOSE 6001

CMD ["hello-world"]

Running Your Application

You have two options to run your Swift application:

Option 1: Run Locally with Swift

You can run the server on your development machine first to make sure everything builds:

swift run

The first build fetches the Hummingbird and OpenTelemetry dependencies, then starts the server. It will keep running and listening on 0.0.0.0:6001. In another terminal you can test it:

curl http://localhost:6001
# {"message":"hello-world"}

Press Ctrl+C to stop the local server.

Option 2: Deploy to a WendyOS Device

Use the Wendy CLI to cross-compile, push, and start your app on a connected device:

wendy run

Wendy cross-compiles the binary with Swift 6.3 for the target device, deploys it, and streams logs. Once the readiness probe passes, the postStart hook opens http://${WENDY_HOSTNAME}:6001 in your browser, where you'll see:

{"message":"hello-world"}

Verifying Deployment on Your Device

After deploying, you can verify the app is running by listing the applications on your device:

wendy device apps list
wendy device apps list showing the running hello-world app

You should see output similar to:

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        │
╰───────────────┴─────────┴─────────┴──────────╯

Expected State: The app shows as "Running" because it is a long-running HTTP server that keeps listening for requests. The "Failures" column showing 0 confirms your application started successfully without errors. To stop it later, run wendy device apps stop hello-world.

Next Steps

Now that you have a basic Swift server running:

  • Add more routes to handle different endpoints (see the Simple Web Server guide)
  • Explore WendyOS frameworks for hardware access
  • Build more complex applications with user input and data processing

Local vs Device: swift run compiles and runs your application locally on your development machine. wendy run cross-compiles with Swift 6.3 for the target device and deploys it automatically.

On this page