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.
- Features
- Requirements
- Installation
- Getting Started
- Configuration
- Environments
- Routing
- Route Groups
- Middleware
- Error Handling
- Request
- Response
- Running and Shutting Down
- Testing
- Contributing
- License
- 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
Encodablevalue and the framework wraps it in a200 OKresponse automatically, or return an explicitResponsefor full control - Environment-aware — built-in
production,development, andtestingenvironments configurable via a process variable - Modular architecture — HTTP and routing are standalone packages that can be used independently
| Chaqmoq | Swift | Platforms |
|---|---|---|
master |
5.10+ | macOS 12+, Ubuntu |
Chaqmoq CLI is a companion command-line tool that scaffolds new applications, runs them from the terminal, and opens them in Xcode.
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/chaqmoqchaqmoq new --name MyApp
# or
chaqmoq new -n MyAppThis clones the official Chaqmoq template, injects the application name into Package.swift, removes template-specific files, and runs an initial swift build.
chaqmoq runPass --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 stagingIf 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.
chaqmoq xcodeOpens 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 MyAppAdd 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 ChaqmoqThe 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 runOpen Sources/MyApp/main.swift — this is the entry point of your application.
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 runThe 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.
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.) |
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 runBuilt-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 setTwo Environment values are equal when their names match:
Environment(name: "staging") == Environment(name: "staging") // trueRoutes 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 let you share a common path prefix and optional name prefix across multiple related routes, keeping registration concise and organised.
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".
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
}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/postsGroups 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 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 runs for every request. Assign an array to app.middleware — RoutingMiddleware 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 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
}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
}
}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.
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!"
}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)
}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()Run the full test suite with:
swift testWhen 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)
}
}Contributions are welcome. Please read the Contributing Guide before opening a pull request:
- Fork the repository and create a branch from
master - Add tests for any new behaviour
- Update documentation for any API changes
- Ensure
swift testpasses and the code lints cleanly - 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).
Chaqmoq is released under the MIT License.
