Introduction

Alchemy provides a lightweight, async enabled, wrapper around the official Swift on the Server async-http-client. You get a high performance, easy to use interface that makes it easy for your app to communicate with other web applications.

Making Requests

Out of the box, a default client is registered to your application’s service container and comes with a request builder, conveniently accessible through the Http alias. You may use the get, post, put, patch, delete, etc functions to make requests.

let response = try await Http.get("https://swift.org")

Each request returns a Client.Response object that contains a variety of methods to help handle the response.

// Status
response.isOk // Bool
response.isSuccessful // Bool
response.isFailed // Bool
response.isClientError // Bool
response.isServerError // Bool
response.validateSuccessful() // throws -> Bool

// Headers
response.header("Header-Name") // String?

// Content
response.body // ByteContent?
response.data // Data?
response.string // String?
response.decode<D: Decodable>(type: D, using: ContentDecoder) // throws -> D

Like Request, Client.Response also conforms to ContentInspector so you can use subscripts and dynamic typing to access various fields the response.

let response = try await Http.get("https://example.com/users/1")
let userName = try response["name"].string
let userAge = try response.age.int

Request Content

The reqest builder conforms to RequestBuilder and contains many methods for constructing requests. If you want to include some JSON content on your requests, you should use the .withJSON() function which takes a dictionary.

let response = try await Http.withJSON([
    "name": "Josh",
    "age": 28
]).post("https://example.com/users")

You may also pass an Encodable type, optionally speciying the JSONEncoder with which to encode.

struct User: Codable {
    let name: String
    let age: Int
}

let user = User(name: "Louis", age: 34)
let response = try await Http
    .withJSON(user)
    .post("https://example.com/users")

Queries

If you’d like to specify a url query on the request, you may specify it directly in the url or use either withQuery() (for a single key / value pair) or withQueries() (for multiple query items).

let response = try await Http
    .withQuery("name", value: "Rachel")
    .get("https://example.com/users")

let response = try await Http
    .withQueries([
        "job": "Software Engineer",
        "page": 1,
    ])
    .get("https://example.com/users")

Making Form URL Encoded Requests

The .withForm() method lets you send data using the application/x-www-form-urlencoded content type. Like withJSON(), it takes either a dictionary or an Encodable type and an optional URLEncodedFormEncoder.

let response = try await Http.withForm([
    "name": "Josh",
    "age": 28
]).post("https://example.com/users")

let user = User(name: "Cassidy", age: 26)
let response = try await Http.withForm(user).post("https://example.com/users")

Sending a Raw Request Body

Of course, you have full customization over the content of your request. If you’d prefer to send a raw request body, you can use the .withBody() to send either Foundation.Data or a swift-nio ByteBuffer.

// Foundation.Data
let data: Data = requestString.data(using: .utf8)!
let response = try await Http.withBody(data).post("https://example.com/photo")

// swift-nio ByteBuffer
let buffer = ByteBuffer(string: requestString)
let response = try await Http.withBody(buffer).post("https://example.com/photo")

Multipart Requests

If you need to make a multipart/form-data request, you’ll use the attach() method. This takes a name, an attachment contents, and an optional filename.

let imageData: ByteBuffer = ...
let response = try await Http
    .attach("image", contents: imageData, filename: "photo.jpg")
    .post("https://example.com/photo")

You can also attach a streamed File or multiple File resources directly.

let image: File = Storage.get("images/kitten.jpg")
let upload: File = request.file("profile")!
let response = try await Http.attach([
    "kitten": image,
    "profile": upload
]).post("https://example.com/photos")

Headers

A header may be added to a request using the withHeader() method. Multiple headers may be added using withHeaders().

let response = try await Http.withHeaders([
    "X-One": "foo",
    "X-Two": "bar"
]).get("https://example.com/users")

Authentication

You may specify basic authentication credentials using withBasicAuth().

let response = try await Http.withBasicAuth(username: "josh@withapollo.com", password: "secret").get(...)

Bearer Token

If you’d like to quickly add a bearer token to the request, you may use the withToken() method.

let response = Http.withToken("token").get(...)

Timeout

You may specify a timeout for your request using withTimeout(), after which your request will be terminated and an error will be thrown.

let response = try await Http.withTimeout(.seconds(5)).get(...)

Response Streaming

By default, any Client.Response will have it’s body accumulated into a single ByteBuffer before returning, so that you don’t need to worry about streamed responses. If you would like to explicitly allow a streamed response, you may do so with the withStream() function.

let response = try await Http.withStream().get("https://example.com/large_download")
let stream: ByteStream = response.body!.stream

Custom Configurations

Under the hood, all you requests are made by the official Swift Server async-http-client library. If you’d like finer grain customization over the internal client configuration, you may specify one using withClientConfig().

import AsyncHTTPClient

let config = HTTPClient.Configuration(...)
let response = try await Http.withClientConfig(config).get(...)

Testing

To help you write isolated, expressive tests, the Client interface includes a stub() method allowing you to respond to requests with mock responses.

Stubbing Responses

To return a 200 for all outgoing requests on a client, you may use the stub() function.

Http.stub()

let response = try await Http.get(...) // 200 with empty body

Stubbing Specific URLs

If you’d like to provide custom stubbed responses for specific URLs, you may pass a dictionary of url patterns & responses to the stub() method. If a request doesn’t match any of the provided patterns, it will result in a 200 response. * is treated as a wildcard. The static stub() function on Client.Response lets you quickly create stubbed responses.

Http.stub([
    // Stub a string response for all facebook.com endpoints
    "facebook.com/*": .stub(.ok, body: "Hello There!"),

    // Stub a json response for all Stripe endpoints
    "api.stripe.com/*": .stub(.ok, body: .json([
        "foo": "value",
        "bar": 1,
    ])),
])

Stubbing With A Handler

If you’d prefer fine grain control over stubbing each request’s response, you may pass a closure to the stub function. With it, you can perform whatever logic you need to mock the given request. It take a single Client.Request parameter and must return a Client.Response.

Http.stub { request in
    return .stub(.ok, body: "Hello from \(request.path)!")
}

Validating Requests

If you’d like to validate the requests your app is making during testing, you may use the assertSent() function in the AlchemyTest library. It takes an optional count of expected requests, as well as an optional closure for verifying the request contents. When verifying the request, you may use the RequestInspector interface as well as the hasPath(), hasHeader(), hasPath(), and hasMethod() sugar functions.

import AlchemyTest

Http.mock()

let body = User(name: "Cyanea", age: 34)
Http
    .withHeader("X-Foo", value: "bar")
    .withJson(body)
    .post("https://example.com/users")

Http.assertSent(1) {
    $0.hasPath("/users") &&
    $0.hasMethod(.POST) &&
    $0.hasHeader("X-Foo", value: "bar") &&
    $0["name"] == "Cyanea" &&
    $0["age"] == 34
}

If you’d like to assert that nothing was sent, use the assertNothingSent() function.

Http.mock()

Http.assertNothingSent()