Routing
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
}
}