Skip to content

chaqmoq/chaqmoq

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

182 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Chaqmoq is a non-blocking server-side web framework consisting of a set of reusable standalone packages and powered by fast, secure, and powerful Swift language and SwiftNIO. Read the documentation for more info.

Table of Contents

Features

  • Non-blocking I/O — built on SwiftNIO for high-throughput, event-driven networking
  • Trie-based router — fast route resolution with support for dynamic parameters
  • Flexible middleware — composable app-level and route-level middleware pipelines
  • Ergonomic handlers — return any Encodable value and the framework wraps it in a 200 OK response automatically, or return an explicit Response for full control
  • Environment-aware — built-in production, development, and testing environments configurable via a process variable
  • Modular architecture — HTTP and routing are standalone packages that can be used independently

Requirements

Chaqmoq Swift Platforms
master 5.10+ macOS 12+, Ubuntu

Installation

Chaqmoq CLI

Chaqmoq CLI is a companion command-line tool that scaffolds new applications, runs them from the terminal, and opens them in Xcode.

Installation

Clone the repository and build the release binary:

git clone https://github.com/chaqmoq/cli.git
cd cli
swift build -c release
cp .build/release/Run /usr/local/bin/chaqmoq

Create a new application

chaqmoq new --name MyApp
# or
chaqmoq new -n MyApp

This clones the official Chaqmoq template, injects the application name into Package.swift, removes template-specific files, and runs an initial swift build.

Run the application

chaqmoq run

Pass --env (or -e) to set the runtime environment. The CLI loads the corresponding .env.<environment> file and sets CHAQMOQ_ENV automatically:

chaqmoq run --env production
chaqmoq run -e staging

If no environment is specified it defaults to development. The app listens on port 8080 by default — open http://localhost:8080 once it starts. The CLI also handles SIGTERM and SIGINT gracefully, releasing port 8080 on shutdown.

Open in Xcode

chaqmoq xcode

Opens the application's Package.swift in Xcode. Pass --name (or -n) when running the command from outside the project directory:

chaqmoq xcode --name MyApp
chaqmoq xcode -n MyApp

Manually

Add Chaqmoq to your Package.swift:

// swift-tools-version:5.10

import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [
        .macOS(.v12)
    ],
    dependencies: [
        .package(url: "https://github.com/chaqmoq/chaqmoq.git", branch: "master")
    ],
    targets: [
        .executableTarget(
            name: "MyApp",
            dependencies: [
                .product(name: "Chaqmoq", package: "chaqmoq")
            ]
        )
    ]
)

Then import the framework in your source files:

import Chaqmoq

Getting Started

Using the CLI

The fastest way to get started is with the Chaqmoq CLI. Run the following to scaffold, enter, and start your project:

chaqmoq new --name MyApp
cd MyApp
chaqmoq run

Open Sources/MyApp/main.swift — this is the entry point of your application.

Manually

Create a new Swift executable package, add Chaqmoq as a dependency in Package.swift (see Installation), then create Sources/<AppName>/main.swift with:

import Chaqmoq

let app = Chaqmoq()

app.get { _ in
    "Hello, World!"
}

try app.run()

Start the server with:

swift run

The app listens on port 8080 by default — open http://localhost:8080 once it starts.

run() blocks the calling thread until the server stops. Call shutdown() from another thread or signal handler to stop it gracefully.

Configuration

Chaqmoq accepts a Configuration value that controls the application identifier, the static files directory, and the underlying server settings.

let configuration = Chaqmoq.Configuration(
    identifier: "com.myapp",
    publicDirectory: "Public",
    server: .init()
)

let app = Chaqmoq(configuration: configuration)
Parameter Type Default Description
identifier String "dev.chaqmoq" A unique identifier for the application, such as a reverse domain name
publicDirectory String "Public" Path to the static files directory, relative to the working directory
server Server.Configuration .init() Configuration for the underlying HTTP server (port, TLS, etc.)

Environments

Chaqmoq supports runtime environments to vary behaviour across development, testing, and production without code changes.

// Use a built-in preset
let app = Chaqmoq(environment: .production)

// Use a custom environment
let app = Chaqmoq(environment: Environment(name: "staging"))

The environment defaults to the value of the CHAQMOQ_ENV process variable, falling back to .development if the variable is absent or empty:

CHAQMOQ_ENV=production swift run

Built-in presets:

Preset Name Intended use
.development "development" Local development (default)
.production "production" Live deployments
.testing "testing" Automated test runs

Read any process environment variable at runtime using:

let region = Environment.get("AWS_REGION")  // String? — nil if not set

Two Environment values are equal when their names match:

Environment(name: "staging") == Environment(name: "staging")  // true

Routing

Routes are registered directly on the Chaqmoq instance. Handlers receive a Request and return any Encodable value or an explicit Response.

// Return a plain value — automatically wrapped in a 200 OK response
app.get { _ in
    "Hello, World!"
}

// Return a Codable struct
app.get("users") { _ in
    [User(id: 1, name: "Alice"), User(id: 2, name: "Bob")]
}

// Return an explicit Response for full control over status and headers
app.post("users") { request in
    let user = try request.body.decode(User.self)
    return Response(status: .created)
}

Route parameters:

Parameters use curly brace notation and support several forms:

Syntax Description
{id} Captures any string
{id<\\d+>} Captures a value matching a regex
{page?1} Optional — falls back to 1 if omitted
{id!1} Forced default — must be explicitly provided, defaults to 1
{id<\\d+>?1} Regex constraint combined with optional default
app.get("users/{id<\\d+>}") { request in
    let id = request.route?[parameter: "id"] as? Int
    return "User \(id ?? 0)"
}

app.get("posts/{page?1}") { request in
    let page = request.route?[parameter: "page"] as? Int
    return "Page \(page ?? 1)"
}

Typed parameter extraction is available via subscript — route?[parameter: "id"] can be cast to Int?, String?, UUID?, or Date? depending on the captured value.

Constant segments always take priority over parameters at the same position, so GET /posts/latest and GET /posts/{id<\\d+>} can coexist without conflict.

Supported HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS

Route Groups

Route groups let you share a common path prefix and optional name prefix across multiple related routes, keeping registration concise and organised.

Closure-based groups

group(_:name:_:) takes a path prefix, an optional name prefix, and a closure that receives a group object for registering nested routes:

app.group("/api/v1", name: "api.v1.") { v1 in
    v1.get("/users", name: "users.index") { _ in ... } // GET /api/v1/users
    v1.post("/users", name: "users.create") { _ in ... } // POST /api/v1/users
    v1.get("/users/{id}", name: "users.show") { _ in ... } // GET /api/v1/users/{id}
    v1.put("/users/{id}", name: "users.update") { _ in ... } // PUT /api/v1/users/{id}
    v1.delete("/users/{id}", name: "users.delete") { _ in ... } // DELETE /api/v1/users/{id}
}

The name prefix is composed automatically, so name: "users.index" inside a "api.v1." group becomes the full route name "api.v1.users.index".

Group middleware

Groups also accept a middleware parameter. The group-level middleware is prepended to any route-level middleware defined inside the group:

app.group("/admin", middleware: [AuthMiddleware()]) { admin in
    admin.get("/dashboard") { _ in ... } // AuthMiddleware runs first
    admin.get("/users", middleware: [LoggingMiddleware()]) { _ in ... } // AuthMiddleware, then LoggingMiddleware
}

Value-returning groups

grouped(_:name:) returns a group object for use outside a closure:

let v2 = app.grouped("/api/v2", name: "api.v2.")
v2.get("/posts") { _ in ... }  // GET /api/v2/posts

Nested groups

Groups can be nested to any depth — path and name prefixes compose automatically at each level:

app.group("/api") { api in
    api.group("/v1") { v1 in
        v1.get("/posts") { _ in ... }  // GET /api/v1/posts
    }
    api.group("/v2") { v2 in
        v2.get("/posts") { _ in ... }  // GET /api/v2/posts
    }
}

Middleware

Middleware intercepts requests before they reach a route handler. It can inspect or modify the request, short-circuit with a response, or pass control to the next step in the pipeline.

App-level middleware

App-level middleware runs for every request. Assign an array to app.middlewareRoutingMiddleware is always appended last automatically and should not be included manually.

app.middleware = [
    LoggingMiddleware(),
    CORSMiddleware(),
    AuthMiddleware()
]

Middleware executes in array order. In the example above, LoggingMiddleware runs first, then CORSMiddleware, then AuthMiddleware, and finally routing resolves the request to its handler.

Route-level middleware

Route-level middleware runs only for a specific route, after app-level middleware:

app.get("admin", middleware: [RequireAdminMiddleware()]) { request in
    "Admin panel"
}

Multiple route-level middleware are applied in order:

app.post("upload", middleware: [AuthMiddleware(), RateLimitMiddleware()]) { request in
    // handle upload
}

Writing middleware

Conform to the Middleware protocol and implement handle(request:responder:). Call responder(request) to pass control to the next middleware or the route handler.

struct LoggingMiddleware: Middleware {
    func handle(request: Request, responder: @escaping Responder) async throws -> Encodable {
        print("\(request.method) \(request.url.path)")
        let response = try await responder(request)
        print("\(response)")
        return response
    }
}

Error Handling

Assign ErrorMiddleware conformers to app.errorMiddleware to handle errors thrown anywhere in the middleware pipeline or route handlers.

Conform to ErrorMiddleware and implement handle(request:error:responder:). Call responder(request, error) to forward to the next error middleware in the chain, or return a response directly to short-circuit:

struct AppErrorMiddleware: ErrorMiddleware {
    func handle(request: Request, error: Error, responder: @escaping ErrorResponder) async throws -> Encodable {
        switch error {
        case let abort as AbortError:
            return Response(status: abort.status)
        default:
            return Response(status: .internalServerError)
        }
    }
}

app.errorMiddleware = [AppErrorMiddleware()]

Multiple error middleware are applied in order. Each conformer receives the request, the thrown error, and a responder closure — call responder(request, error) to pass the error to the next error middleware, or return a response to stop propagation.

Request

The Request object is passed to every middleware and route handler, exposing headers, body, URL, and route parameters.

app.get("greet/{name}") { request in
    let name = request.route?[parameter: "name"] as? String ?? "stranger"
    return "Hello, \(name)!"
}

Typed parameter extraction is done via request.route?[parameter:], which returns String?, Int?, UUID?, or Date? depending on the captured value:

app.get("users/{id<\\d+>}") { request in
    let id = request.route?[parameter: "id"] as? Int
    return "User \(id ?? 0)"
}

Matched route: Once routing resolves, the matched Route is available via request.route. This property is nil in app-level middleware, since routing has not yet run at that point.

struct LoggingMiddleware: Middleware {
    func handle(request: Request, responder: @escaping Responder) async throws -> Encodable {
        // request.route is nil here — routing resolves after app-level middleware
        let response = try await responder(request)
        return response
    }
}

app.get("hello") { request in
    // request.route is set here
    print(request.route?.path ?? "unknown")
    return "Hello!"
}

Response

Return any Encodable value from a route handler and the framework serialises it and wraps it in a 200 OK response:

app.get { _ in "Hello" } // 200 OK, body: "Hello"
app.get("count") { _ in 1 } // 200 OK, body: 1
app.get("user") { _ in // 200 OK, body: JSON-encoded User
    User(id: 1, name: "Alice")
}

Return an explicit Response when you need control over the status code or headers:

app.post("users") { request in
    return Response(status: .created)
}

app.delete("users/{id}") { request in
    return Response(status: .noContent)
}

Running and Shutting Down

run() starts the server and blocks the calling thread. It throws if the server fails to start:

try app.run()

shutdown() stops the server and releases its resources. Because run() blocks, shutdown() must be called from a different thread — for example, from a signal handler:

let source = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .global())
source.setEventHandler { try? app.shutdown() }
source.resume()

try app.run()

A DispatchSemaphore can guarantee that shutdown() is called only after the server is fully started:

let semaphore = DispatchSemaphore(value: 0)
app.server.onStart = { _ in semaphore.signal() }

DispatchQueue.global().async {
    semaphore.wait()
    try? app.shutdown()
}

try app.run()

Testing

Run the full test suite with:

swift test

When writing tests for your own routes, use Environment.testing and EmbeddedEventLoop from SwiftNIO for a lightweight, synchronous event loop:

import XCTest
@testable import Chaqmoq

final class MyRouteTests: XCTestCase {
    func testGreetRoute() async throws {
        // Arrange
        let app = Chaqmoq(environment: .testing)
        let middleware = RoutingMiddleware(router: app)
        let request = Request(eventLoop: EmbeddedEventLoop())
        app.get { _ in "Hello, World!" }

        // Act
        let result = try await middleware.handle(request: request) { _ in fatalError() }
        let response = try XCTUnwrap(result as? Response)

        // Assert
        XCTAssertEqual(response.status, .ok)
    }
}

Contributing

Contributions are welcome. Please read the Contributing Guide before opening a pull request:

  1. Fork the repository and create a branch from master
  2. Add tests for any new behaviour
  3. Update documentation for any API changes
  4. Ensure swift test passes and the code lints cleanly
  5. Open a pull request

Bug reports and feature requests go through GitHub Issues. The project follows Swift's API Design Guidelines and enforces style via SwiftLint (4-space indentation, 120-character line limit).

License

Chaqmoq is released under the MIT License.

About

A non-blocking server-side web framework in Swift

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Contributors

Languages