Handling Requests

When a request comes through the host & port on which your server is listening, it immediately gets routed to your application You can set up handlers in your application’s boot() function.

Handlers are defined with the .on(method:at:handler:) function, which takes an HTTPMethod, a path, and a handler. The handler is a closure that accepts a Request and returns a type that conforms to ResponseConvertable.

There’s sugar for registering handlers for specific methods via get(), post(), put(), patch(), etc.

struct ExampleApp: Application {
    func boot() {
        // GET {host}:{port}/hello
        get("/hello") { req in
            "Hello, World!"
        }

        // Handlers can be async and throw errors.
        get("/users") { req in
            try await DB.table("users").get()
        }
    }
}

Returning a Value

Handlers can be asynchronous and throw errors. They should return either a Response, something conforming to ResponseConvertible, something conforming to Encodable, or Void.

Response

app.get("/response") { req in
    Response(status: .ok, body: "Hello from /response")
}

ResponseConvertible

struct User: ResponseConvertible {
    let name: String

    public func response() -> Response {
        Response(status: .ok, body: "Hello, \(name)!")
    }
}
app.get("/response") { req in
    User(name: req["name"])
}

Encodable

/// String
app.get("/string") { req in
    "Howdy!"
}

/// Custom Encodable type
app.get("/todo") { req in
    Todo(name: "Write backend Swift", isDone: true)
}

struct Todo: Encodable {
    let name: String
    let isDone: Bool
}

Void

app.get("/testing_query") { request in
    print("Got params \(request.queryItems)")
}

Chaining Requests

To keep code clean, handlers are chainable.

let controller = UserController()
app
    .post("/user", handler: controller.create)
    .get("/user", handler: controller.get)
    .put("/user", handler: controller.update)
    .delete("/user", handler: controller.delete)

Error Handling

If a handler throws an error, it will be caught & automatically mapped to a Response.

Generic errors will result in an Response with a status code of 500, but if any error that conforms to ResponseConvertible is thrown, it will be converted as such.

Out of the box, Alchemy provides HTTPError which conforms to ResponseConvertible. If it is thrown, the response will contain the status code & message of the error.

struct MyError: Error { ... }

app
    .get("/500") { req in
        // Will result in a 500 response with a generic error message.
        throw MyError()
    }
    .get("/404") { req in
        // Will result in a 404 response with the custom message.
        throw HTTPError(status: .notFound, message: "This endpoint doesn't exist!")
    }

Custom Default Error Handler

By default, unhandled errors will result in a 500 response. If you’d prefer, you can use setErrorHandler to return a custom Response whenever your app encounters an unhandled error.

app.setErrorHandler { req, err in
    Response(status: .internalServerError, message: "Uh oh. We've hit a snag.")
}

Custom Not Found Handler

Likewise, use notFoundHandler to return a custom Response when a request is made that doesn’t match any of your app’s routes.

app.notFoundHandler { req in
    Response(status: .notFound, message: "These aren't the bytes you're looking for.")
}

Route Parameters

Dynamic route parameters can be added with a variable name prefaced by a colon (:). The value of the parameter can be accessed on the Request object.

app.get("/users/:userId") { req in
    let id = req.parameter("userId", as: Int.self)
}

As long as they have different names, a route can have as many path parameters as you’d like.

Route Groups

You can use .group() to add middleware or path prefixes to a specific group of routes.

app.group(AuthMiddleware())
    .get("/user") { /* will run AuthMiddleware beforehand */ }

app.post("/login") { /* will not run AuthMiddleware */ }

There is also a closure based .group() that will isolate the prefix or middleware to any handlers defined in the closure. Using the closure based version, the example above would look like so:

app
    .group(AuthMiddleware()) {
        $0.get("/user") { ... }
    }
    .post("/login")

Route Streaming

By default, Alchemy collects the entire body of a Request before a route handler is called. If the request is uploading a large file, this could balloon memory. If you’d like to enable request streaming, pass the .stream to a handler’s options parameter. This will call the handler as soon as the request’s body begins streaming so you can handle each chunk as it comes in.

app.post("/large_file", options: .stream) { req in
    req.body?.stream.readAll { chunk in
        // process chunk
    }
}