How can I have Hummingbird serve static files?

So I’ve already mentioned Hummingbird a few times. Most recently I showed off the “simplest server” in the how to deploy to an App server post.

This time I want to show how to set up Hummingbird to host static pages. Hummingbird primarily focuses on serving Swift generated content, but static pages can be incredibly useful for testing APIs.

This example started from a direct copy of https://github.com/adam-fowler/swift-web on 2025, Aug 15, and honestly did not change much.

It’s a utility like python3 -m http.server that will start a servlet based on the contents of a directory. I’ll talk about the install later.

The Code

The code is all of 35 lines. It’s really simple. It depends on Hummingbird and ArgumentParser to make a 1 page app.

FileMiddleware is provided by Hummingbird, but it’s possible to write custom versions. Middleware in this context is kind of like a plugin that a web app developer can insert between the request and the response, altering Hummingbird’s default behaviors. Normally if a route isn’t cataloged in the Router, Hummingbird would send a 404 Response. With FileMiddleware registered, instead Hummingbird will look in the specified directory for a path that matches the route first.

import ArgumentParser
import Hummingbird
import Logging

@main
struct WebServer: AsyncParsableCommand {
    @Option(name: .shortAndLong)
    //will cause issue in containers but this is for
    //running locally.
    var hostname: String = "127.0.0.1"

    @Option(name: .shortAndLong)
    var port: Int = 8080

    @Argument
    var folder: String = "."

    func run() async throws {

        print(folder)

        let logger = Logger(label: "swift-serve")
        let router = Router()
        router.middlewares.add(FileMiddleware(self.folder, searchForIndexHtml: true, logger: logger))
        router.middlewares.add(LogRequestsMiddleware(.info))
        
        let app = Application(
            router: router,
            configuration: .init(
                address: .hostname(self.hostname, port: self.port),
                serverName: "swift-serve"
            ),
            logger: logger
        )
        try await app.runService()
    }
}

The Package.swift

// swift-tools-version:6.1
import PackageDescription

let package = Package(
    name: "swift-serve",
    platforms : [.macOS(.v14)],
    products: [.executable(name: "swift-serve", targets: ["swift-serve"])],
    dependencies: [
        .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0")
    ],
    targets: [
        .executableTarget(
            name: "swift-serve",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                .product(name: "Hummingbird", package: "hummingbird"),
            ]
        ),
    ]
)

Usage

If one goes to the example’s repo, there’s a folder called “public” there with some HTML for testing. The lines

    @Argument
    var folder: String = "."

mean that the server will treat the present working directory as the root folder if nothing else gets specified. The FileMiddleware will actually look for a folder called public in the package root if nothing is specified, but in this example I put the demo directory right outside the root to give an excuse to show passing a path.

# from the directory with the README / 
cd swift-serve
swift run swift-serve ../public

Installing the Tool

It’d hardly be convenient to keep recompiling or go digging in a build folder whenever it’s time to run a server.

Fowler provides instruction on how to install the server tool using mint. It handles where to put and where to find all the compiled binaries really nicely.

But for fun, let’s go through the exercise of installing the hand written debug version of the code in a folder called ~/mybin (Although one could certainly make a release version first: swift build --configuration release). I prefer to do this with tools I’ve written, rather than putting them in /usr/local/bin or /usr/bin.

First step, copy the executable. A symlink would also be an option if you wanted it to stay up to date as you developed. (ln -s /path/to/original /path/to/link)

## make the folder
mkdir ~/mybin
## From Directory with this readme, after having run `swift build` at the very least. 
cd swift-serve/.build/debug
cp swift-serve ~/mybin/swift-serve
cd -
cd public
~/mybin/swift-serve

This should run the server. If you’d like to add the ~/mybin to the path

# see all the folders in your path
echo $PATH 
## then either add it to the beginning to search it first
PATH=~/mybin:$PATH
## OR add it to the end to search it last. 
PATH=$PATH:~/mybin
## check your path again
echo $PATH

This will ONLY work for THIS shell. To update your profile try seeing what you currently have, e.g. cat ~/.zprofile if using zsh or ls -al ~/ to go hunting.

Add something like

if [[ ":$PATH:" != *":~/mybin:"* ]]; then
    export PATH="Users/$USER/mybin:$PATH"
fi

at the bottom of the file to add your custom folder at the front of the PATH’s line every time you open a shell controlled by that profile.

CAUTION: This means programs in ~/mybin always win. If you name your program the same as something else important, that’ll be some serious woe.

Open a new terminal window (or hash -r the current one) and

cd public
swift-serve

should work now!