Now fully
async/await
!
Welcome to Alchemy, an elegant, batteries included backend framework for Swift. You can use it to build a production ready backend for your next mobile app, cloud project or website.
@main
struct App: Application {
func boot() {
get("/") { req in
"Hello World!"
}
}
}
Alchemy provides you with Swifty APIs for everything you need to build production-ready backends. It makes writing your backend in Swift a breeze by easing typical tasks, such as:
- Simple, fast routing engine.
- Powerful dependency injection container.
- Expressive, Swifty database ORM.
- Database agnostic query builder and schema migrations.
- Robust job queues backed by Redis or SQL.
- First class support for Plot, a typesafe HTML DSL.
- Supporting libraries to share typesafe backend APIs with Swift frontends.
Swift on the server is exciting but also relatively nascant ecosystem. Building a backend with it can be daunting and the pains of building in a new ecosystem can get in the way.
The goal of Alchemy is to provide a robust, batteries included framework with everything you need to build production ready backends. Stay focused on building your next amazing project in modern, Swifty style without sweating the details.
1. Batteries Included
With Routing, an ORM, advanced Redis & SQL support, Authentication, Queues, Cron, Caching and much more, import Alchemy
gives you all the pieces you need to start building a production grade server app.
2. Convention over Configuration
APIs focus on simple syntax with lots of baked in convention so you can build much more with less code. This doesn't mean you can't customize; there's always an escape hatch to configure things your own way.
3. Rapid Development
Alchemy is designed to help you take apps from idea to implementation as swiftly as possible.
4. Interoperability
Alchemy is built on top of the lightweight, blazingly fast Hummingbird framework. It is fully compatible with existing swift-nio
and vapor
components like stripe-kit, soto or jwt-kit so that you can easily integrate with all existing Swift on the Server work.
5. Keep it Swifty
Swift is built to write concice, safe and elegant code. Alchemy leverages it's best parts to help you write great code faster and obviate entire classes of backend bugs. With v0.4.0 and above, it's API is completely async/await
meaning you have access to all Swift's cutting edge concurrency features.
To get started check out the extensive docs starting with Setup.
The Docs provide a step by step walkthrough of everything Alchemy has to offer. They also touch on essential core backend concepts for developers new to server side development. Below are some of the core pieces.
If you'd prefer to dive into some code, check out the example apps in the alchemy-examples repo.
Each Alchemy project starts with an implemention of the Application
protocol. It has a single function, boot()
for you to set up your app. In boot()
you'll define your configurations, routes, jobs, and anything else needed to set up your application.
Routing is done with action functions get()
, post()
, delete()
, etc on the application.
@main
struct App: Application {
func boot() {
post("/hello") { req in
"Hello, \(req.query("name")!)!"
}
}
}
Route handlers can also be async using Swift 5.5's new concurrency features.
get("/download") { req in
// Fetch an image from another site.
try await Http.get("https://example.com/image.jpg")
}
Route handlers will automatically convert returned Codable
types to JSON. You can also return a Response
if you'd like full control over the returned content & it's encoding.
struct Todo: Codable {
let name: String
let isComplete: Bool
let created: Date
}
app.post("/json") { req -> Todo in
return Todo(name: "Laundry", isComplete: false, created: Date())
}
app.get("/xml") { req -> Response in
let xmlData = """
<note>
<to>Rachel</to>
<from>Josh</from>
<heading>Message</heading>
<body>Hello from XML!</body>
</note>
""".data(using: .utf8)!
return Response(
status: .accepted,
headers: ["Content-Type": "application/xml"],
body: .data(xmlData)
)
}
Bundling groups of routes together with controllers is a great way to clean up your code. This can help organize your projects by resource type.
struct TodoController: Controller {
func route(_ app: Application) {
app
.get("/todo", getAllTodos)
.post("/todo", createTodo)
.patch("/todo/:id", updateTodo)
}
func getAllTodos(req: Request) async throws -> [Todo] { ... }
func createTodo(req: Request) async throws -> Todo { ... }
func updateTodo(req: Request) async throws -> Todo { ... }
}
// Register the controller
myApp.controller(TodoController())
Often, you'll want to configure variables & secrets in your app's environment depending on whether your building for dev, stage or prod. The de facto method for this is a .env
file, which Alchemy supports out of the box.
Keys and values are defined per line, with an =
separating them. Comments can be added with a #.
# Database
DB_HOST=localhost
DB_PORT=5432
DB=alchemy
DB_USER=prod
DB_PASSWORD=eLnGRkw55mHEssyP
You can access these variables in your code through the Env
type. If you're feeling fancy, Env
supports dynamic member lookup.
let dbHost: String = Env.current.get("DB_HOST")!
let dbPort: Int = Env.current.get("DB_PORT")!
let isProd: Bool = Env.current.get("IS_PROD")!
let db: String = Env.DB_DATABASE
let dbUsername: String = Env.DB_USER
let dbPass: String = Env.DB_PASS
You can choose a custom env file by passing -e {env} or setting APP_ENV when running your program. The app will load it's environment from the file at .env.{env}
.
Alchemy comes with a powerful query builder that makes it easy to interact with SQL databases. You can always run raw queries as well. DB
is a shortcut to injecting the default database.
try await DB.from("users").select("id").where("age" > 30)
try await DB.raw("SELECT * FROM users WHERE id = 1")
Most SQL operations are supported, including nested WHERE
s and atomic transactions.
// The first user named Josh with age NULL or less than 28
try await DB.from("users")
.where("name" == "Josh")
.where { $0.whereNull("age").orWhere("age" < 28) }
.first()
// Wraps all inner queries in an atomic transaction, will rollback if an error is thrown.
try await DB.transaction { conn in
try await conn.from("accounts")
.where("id" == 1)
.update(values: ["amount": 100])
try await conn.from("accounts")
.where("id" == 2)
.update(values: ["amount": 200])
}
To make interacting with SQL databases even simpler, Alchemy provides a powerful, expressive ORM called Rune. Built on Swift's Codable, it lets you make a 1-1 mapping between simple Swift types and your database tables. Just conform your types to Model and you're good to go. The related table name is assumed to be the type pluralized.
// Backed by table `users`
struct User: Model {
var id: Int?
let firstName: String
let lastName: String
let age: Int
}
try await User(firstName: "Josh", lastName: "Wright", age: 28).insert()
You can easily query directly on your type using the same query builder syntax. Your model type is automatically decoded from the result of the query for you.
try await User.find(1)
// equivalent to
try await User.where("id" == 1).first()
If your database naming convention is different than Swift's, for example snake_case
instead of camelCase
, you can set the static keyMapping
property on your Model to automatially convert to the proper case.
struct User: Model {
static var keyMapping: DatabaseKeyMapping = .convertToSnakeCase
...
}
Relationships are defined via property wrappers & can be eager loaded using .with(\.$keyPath)
.
struct Todo: Model {
...
@BelongsTo var user: User
}
// Queries all `Todo`s with their related `User`s also loaded.
let todos = try await Todo.all().with(\.$user)
for todo in todos {
print("\(todo.title) is owned by \(user.name)")
}
You can customize advanced relationship loading behavior, such as "has many through" by overriding the static mapRelations()
function.
struct User: Model {
@HasMany var workflows: [Workflow]
static func mapRelations(_ mapper: RelationshipMapper<Self>) {
mapper.config(\.$workflows).through("projects")
}
}
Middleware lets you intercept requests coming in and responses coming out of your server. Use it to log, authenticate, or modify incoming Request
s and outgoing Response
s. Add one to your app with use()
or useAll()
.
struct LoggingMiddleware: Middleware {
func intercept(_ request: Request, next: @escaping Next) async throws -> Response {
let start = Date()
let requestInfo = "\(request.head.method) \(request.path)"
Log.info("Received request: \(requestInfo)")
let response = try await next(request)
let elapsedTime = String(format: "%.2fs", Date().timeIntervalSince(start))
Log.info("Sending response: \(response.status.code) \(requestInfo) after \(elapsedTime)")
return response
}
}
// Applies the Middleware to all subsequently defined handlers.
app.use(LoggingMiddleware())
// Applies the Middleware to all incoming requests & outgoing responses.
app.useAll(OtherMiddleware())
You may also add anonymous middlewares with a closure.
app.use { req, next -> Response in
Log.info("\(req.method) \(req.path)")
return next(req)
}
You'll often want to authenticate incoming requests using your database models. Alchemy provides out of the box middlewares for authorizing requests against your ORM models using Basic & Token based auth.
struct User: Model { ... }
struct UserToken: Model, TokenAuthable {
var id: Int?
let value: String
@BelongsTo var user: User
}
app.use(UserToken.tokenAuthMiddleware())
app.get("/user") { req -> User in
req.get(User.self) // The User is now accessible on the request
}
Note that to make things simple for you, a few things are happening under the hood. A tokenAuthMiddleware()
is automatically available since UserToken
conforms to TokenAuthable
. This middleware automatically parse tokens from the Authorization
header of incoming Requests and validates them against the user_tokens
table. If the token matches a UserToken
row, the related User
and UserToken
will be .set()
on the Request for access via get(User.self)
. If there is no match, your server will return a 401: Unauthorized
before hitting the handler.
Also note that, in this case, because Model
descends from Codable
you can return your database models directly from a handler to the client.
Working with Redis is powered by the excellent RedisStack package. Once you register a configuration, the Redis
type has most Redis commands, including pub/sub, as functions you can access.
Redis.bind(.connection("localhost"))
// Elsewhere
@Inject var redis: Redis
let value = redis.lpop(from: "my_list", as: String.self)
redis.subscribe(to: "my_channel") { val in
print("got a \(val.string)")
}
If the function you want isn't available, you can always send a raw command. Atomic MULTI
/EXEC
transactions are supported with .transaction()
.
try await redis.send(command: "GET my_key")
try await redis.transaction { redisConn in
try await redisConn.increment("foo").get()
try await redisConn.increment("bar").get()
}
Alchemy offers Queue
as a unified API around various queue backends. Queues allow your application to dispatch or schedule lightweight background tasks called Job
s to be executed by a separate worker. Out of the box, Redis
, relational databases, and memory backed queues are supported, but you can easily write your own provider by conforming to the QueueProvider
protocol.
To get started, configure the default Queue
and dispatch()
a Job
. You can add any Codable
fields to Job
, such as a database Model
, and they will be stored and decoded when it's time to run the job.
// Will back the default queue with your default Redis config
Queue.config(default: .redis())
struct ProcessNewUser: Job {
let user: User
func run() async throws {
// do something with the new user
}
}
try await ProcessNewUser(user: someUser).dispatch()
Note that no jobs will be dequeued and run until you run a worker to do so. You can spin up workers by separately running your app with the queue
argument.
swift run MyApp worker
If you'd like, you can run a worker as part of your main server by passing the --workers
flag.
swift run MyApp --workers 3
When a job is successfully run, you can optionally run logic by overriding the finished(result:)
function on Job
. It receives the Result
of the job being run, along with any error that may have occurred. From finished(result:)
you can access any of the jobs properties, just like in run()
.
struct EmailJob: Job {
let email: String
func run() async throws { ... }
func finished(result: Result<Void, Error>) {
switch result {
case .failure(let error):
Log.error("failed to send an email to \(email)!")
case .success:
Log.info("successfully sent an email to \(email)!")
}
}
}
For advanced queue usage including channels, queue priorities, backoff times, and retry policies, check out the Queues guide.
Alchemy contains a built in task scheduler so that you don't need to generate cron entries for repetitive work, and can instead schedule recurring tasks right from your code. You can schedule code or jobs from the scheudle()
method of your Application
instance.
@main
struct MyApp: Application {
...
func schedule(schedule: Scheduler) {
// Say good morning every day at 9:00 am.
schedule.run { print("Good morning!") }
.daily(hour: 9)
// Run `SendInvoices` job on the first of every month at 9:30 am.
schedule.job(SendInvoices())
.monthly(day: 1, hour: 9, min: 30)
}
}
A variety of builder functions are offered to customize your schedule frequency. If your desired frequency is complex, you can even schedule a task using a cron expression.
// Every week on tuesday at 8:00 pm
schedule.run { ... }
.weekly(day: .tue, hour: 20)
// Every second
schedule.run { ... }
.secondly()
// Every minute at 30 seconds
schedule.run { ... }
.minutely(sec: 30)
// At 22:00 on every day from Monday through Friday.”
schedule.run { ... }
.cron("0 22 * * 1-5")
Check out the docs for more advanced guides on all of the above as well as Migrations, Caching, Custom Commands, Logging, making HTTP Requests, using the HTML DSL, advanced Request / Response usage, typesafe APIs between client and server, deploying your apps to Linux or Docker, and more.
Alchemy was designed to make it easy for you to contribute code. It's a single codebase with special attention given to readable code and documentation, so feel free to dive in and contribute features, bug fixes, docs or tune ups.
You can report bugs, contribute features, or just say hi on Github discussions and Discord.