Simple server APIs in Swift

Overview

SwiftyBridges

  • Are you or your team working on a server and client in Swift?
  • Are you tired of worrying about HTTP and generating requests and responses?
  • Do you want to skip cobbling together an API client?

SwiftyBridges is here to help! 😎

What is SwiftyBridges?

SwiftyBridges lets you write the server logic in a simple way and then automatically generates an API client plus all communication code for both server and client.

Server code:

import SwiftyBridges
import Vapor

struct HelloAPI: APIDefinition {
    var request: Request
    
    public func hello(firstName: String, lastName: String) -> String {
        "Hello, \(firstName) \(lastName)!"
    }
}

Client code:

import SwiftyBridgesClient

let api = HelloAPI(url: serverURL)

let greeting = try await api.hello(firstName: "Swifty", lastName: "Bridges")
print(greeting)

Requirements

Server: Vapor >= 4.0

Code generation: Xcode 13.0

Client: Swift >= 5.5

Usage

Server

Create an API definition:

import SwiftyBridges

struct IceCreamAPI: APIDefinition {
    var request: Request
    
    public func getAllFlavors() -> [IceCreamFlavor] {
        [
            IceCreamFlavor(name "Chocolate"),
            IceCreamFlavor(name "Vanilla"),
        ]
    }
}

Conform the API definition struct to APIDefinition and make methods that shall be available to the client public.

All parameter and return types of public must conform to Codable (or be futures of Codable types):

struct IceCreamFlavor: Codable {
    var name
}

Create an instance of APIRouter:

import SwiftyBridges

let apiRouter = APIRouter()

Register all API definitions:

apiRouter.register(IceCreamAPI.self)

Set up a POST route for the API router:

EventLoopFuture in apiRouter.handle(req) } ">
app.post("api") { req -> EventLoopFuture<Response> in
    apiRouter.handle(req)
}

Optional Features

API methods may return futures of Codable values:

public func getAllFlavors() -> EventLoopFuture<[IceCreamFlavor]> {
    ...
}

API methods may throw:

public func getAllFlavors() throws -> EventLoopFuture<[IceCreamFlavor]> {
    ...
}

API definitions may use middlewares:

struct IceCreamAPI: APIDefinition {
    static let middlewares: [Middleware] = [
        UserToken.authenticator(),
        User.guardMiddleware(), // <- Optional
    ]
    
    var request: Request
    var user: User
    
    init(request: Request) throws {
        self.request = request
        self.user = try request.auth.require(User.self)
    }
    
    ...
}

Code generation

⚠️ Code generation currently needs the command line tools of Xcode 13.0

Using Mint

Ensure Mint is installed:

$ brew install mint

Then run:

$ mint run SwiftyBridges/[email protected] [path to server package]/Sources/App

(The first time you run this, this may take several minutes.)

This will generate the files ServerGenerated.swift and ClientGenerated.swift. Make sure that these files are in the right directories (ServerGenerated.swift in the server project and ClientGenerated.swift in the client project) and are compiled.

Alternatively, you can add --server-output [path to server package]/Sources/App/Generated.swift --client-output [path to client code]/Generated.swift to the command to directly generate the swift files in the correct places.

Manually

To generate the communication code for both server and client, run the following commands in terminal:

$ git clone https://github.com/SwiftyBridges/SwiftyBridgesVapor.git
$ cd SwiftyBridgesVapor
$ swift run BridgeBuilder [path to server package]/Sources/App

This will generate the files ServerGenerated.swift and ClientGenerated.swift. Make sure that these files are in the right directories (ServerGenerated.swift in the server project and ClientGenerated.swift in the client project) and are compiled.

Alternatively, you can add --server-output [path to server package]/Sources/App/Generated.swift --client-output [path to client code]/Generated.swift to the command to directly generate the swift files in the correct places.

Client

Make sure all Codable types that are used by the API methods are available to the generated code.

Then use the API:

import SwiftyBridgesClient

let api = IceCreamAPI(url: serverURL)

let flavors: [IceCreamFlavor] = try await api.getAllFlavors()

That's it!

Examples

You can find a sample server and client implementation in the Examples repository.

Installation

Server

Add SwiftyBridgesVapor to your Package.swift:

// swift-tools-version:5.5
import PackageDescription

let package = Package(
    name: "MyServer",
    dependencies: [
        .package(url: "https://github.com/SwiftyBridges/SwiftyBridgesVapor.git", .upToNextMinor(from: "0.2.0")),
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "SwiftyBridges", package: "SwiftyBridgesVapor"),
            ]
        ),
    ]
)

Client

Add SwiftyBridgesClient with a version matching the version of SwiftyBridgesVapor used by the server in Xcode or to your Package.swift:

// swift-tools-version:5.2
import PackageDescription

let package = Package(
    name: "MyApp",
    dependencies: [
        .package(url: "https://github.com/SwiftyBridges/SwiftyBridgesClient.git", .upToNextMinor(from: "0.2.0")),
    ],
    targets: [
        .target(
            name: "MyApp",
            dependencies: [
                .product(name: "SwiftyBridgesClient", package: "SwiftyBridgesClient"),
            ]
        ),
    ]
)

Authentication

A simple way to implement authentication is via bearer tokens:

On the server, use BearerAuthenticator or ModelTokenAuthenticatable as described in the Vapor documentation.

For example, if you are using Fluent, conform your token model to ModelTokenAuthenticatable:

extension UserToken: ModelTokenAuthenticatable {
    static let valueKey = \UserToken.$value
    static let userKey = \UserToken.$user

    var isValid: Bool {
        Date() < expirationDate // <- If tokens do not expire, simply return true
    }
}

Then you can restrict one of your API definitions to logged in users:

struct IceCreamAPI: APIDefinition {
    static let middlewares: [Middleware] = [
        UserToken.authenticator(),
        User.guardMiddleware(), // <- Only needed if you don't use the `init()` below.
    ]
    
    var request: Request
    var user: User
    
    init(request: Request) throws {
        self.request = request
        self.user = try request.auth.require(User.self)
    }
    
    ...
}

On the client, you can pass the user token as the bearer token:

let api = IceCreamAPI(url: serverURL, bearerToken: userToken)

Authentication may also be done by:

  • Explicitly passing the user token:
    public func getAllFlavors(userToken: String) -> [IceCreamFlavor]
  • Passing authentication information in the URL query:
    let api = IceCreamAPI(url: serverURLWithConfiguredQuery)
  • Passing authentication information in HTTP headers:
    let api = IceCreamAPI(baseRequest: requestWithPresetHTTPHeaders)

Login

Login may for example be implemented using an unauthenticated API definition like so:

import Fluent
import SwiftyBridges
import Vapor

/// Allows the user to log in and to register an account
struct LoginAPI: APIDefinition {
    var request: Request
    
    /// Allows the user to log in
    /// - Parameters:
    ///   - username: The username of the user
    ///   - password: The password of the user
    /// - Returns: A user token needed to perform subsequent API calls for this user
    public func logIn(username: String, password: String) throws -> EventLoopFuture<String> {
        User.query(on: request.db)
            .filter(\.$name == username)
            .first()
            .flatMapThrowing { foundUser -> UserToken in
                guard
                    let user = foundUser,
                    try user.verify(password: password)
                else {
                    throw Abort(.unauthorized)
                }
                return try user.generateToken()
            }.flatMap { token in
                token.save(on: request.db)
                    .map { token.value }
            }
    }
}

The client can then use the returned user token as the bearer token as explained above.

Login Expiration

If the login has expired, the server can throw an Abort(.unauthorized) or just use a middleware like UserToken.authenticator() in combination with User.guardMiddleware().

On the client-side, this can be handled like this:

let iceCreamAPI = IceCreamAPI(url: serverURL, bearerToken: userToken)

let httpErrors = iceCreamAPI.errors
    .compactMap { $0 as? HTTPError }

Task {
    if await httpErrors.first(where: { $0.isUnauthorizedError }) != nil {
        handleExpiredLogin()
    }
}

Current Limitations

  • SwiftyBridges currently only supports Vapor on the server-side
  • Server-side API methods do not currently support the following features:
    • Default parameter values
    • Variadic parameters
    • async
  • All errors thrown by API methods are currently converted to HTTPError on the client
  • Running code generation as part of the server code compilation is currently not supported. This will hopefully change when Package Plugins land in Swift 5.6.

If any of these limitations is bothering you, please get in touch.

You might also like...
Super lightweight async HTTP server library in pure Swift runs in iOS / MacOS / Linux

Embassy Super lightweight async HTTP server in pure Swift. Please read: Embedded web server for iOS UI testing. See also: Our lightweight web framewor

A light-weight server-side service framework written in the Swift programming language.

Smoke Framework The Smoke Framework is a light-weight server-side service framework written in Swift and using SwiftNIO for its networking layer by de

Tiny http server engine written in Swift programming language.

What is Swifter? Tiny http server engine written in Swift programming language. Branches * stable - lands on CocoaPods and others. Supports the latest

PillowTalk - An iOS & SwiftUI server monitor tool for linux based machines using remote proc file system with script execution.
PillowTalk - An iOS & SwiftUI server monitor tool for linux based machines using remote proc file system with script execution.

An iOS & SwiftUI server monitor tool for linux based machines using remote proc file system with script execution.

A small, lightweight, embeddable HTTP server for Mac OS X or iOS applications

CocoaHTTPServer CocoaHTTPServer is a small, lightweight, embeddable HTTP server for Mac OS X or iOS applications. Sometimes developers need an embedde

GCDWebServer is a modern and lightweight GCD based HTTP 1.1 server designed to be embedded in iOS, macOS & tvOS apps.

GCDWebServer is a modern and lightweight GCD based HTTP 1.1 server designed to be embedded in iOS, macOS & tvOS apps. It was written from scr

iOS Tweak to redirect Discord API calls to a Fosscord server.

FosscordTweak iOS Tweak to redirect Discord API calls to a Fosscord server. Installation Manual Download .deb file from release and install on jailbro

A simple Swift wrapper for libgd

SwiftGD This is a simple Swift wrapper for libgd, allowing for basic graphic rendering on server-side Swift where Core Graphics is not available. Alth

 🪶 Feather is a modern Swift-based content management system powered by Vapor 4.
🪶 Feather is a modern Swift-based content management system powered by Vapor 4.

Feather CMS 🪶 🪶 Feather is a modern Swift-based content management system powered by Vapor 4. 💬 Click to join the chat on Discord. Requirements To

Releases(0.2.0)
Owner
null
Server-side Swift. The Perfect core toolset and framework for Swift Developers. (For mobile back-end development, website and API development, and more…)

Perfect: Server-Side Swift 简体中文 Perfect: Server-Side Swift Perfect is a complete and powerful toolbox, framework, and application server for Linux, iO

PerfectlySoft Inc. 13.9k Dec 29, 2022
Swift backend / server framework (Pure Swift, Supports Linux)

NetworkObjects NetworkObjects is a #PureSwift backend. This framework compiles for OS X, iOS and Linux and serves as the foundation for building power

Alsey Coleman Miller 258 Oct 6, 2022
Tiny http server engine written in Swift programming language.

What is Swifter? Tiny http server engine written in Swift programming language. Branches * stable - lands on CocoaPods and others. Supports the latest

null 3.6k Dec 31, 2022
Swift HTTP server using the pre-fork worker model

Curassow Curassow is a Swift Nest HTTP Server. It uses the pre-fork worker model and it's similar to Python's Gunicorn and Ruby's Unicorn. It exposes

Kyle Fuller Archive 397 Oct 30, 2022
Lightweight library for web server applications in Swift on macOS and Linux powered by coroutines.

Why Zewo? • Support • Community • Contributing Zewo Zewo is a lightweight library for web applications in Swift. What sets Zewo apart? Zewo is not a w

Zewo 1.9k Dec 22, 2022
💧 A server-side Swift HTTP web framework.

Vapor is an HTTP web framework for Swift. It provides a beautifully expressive and easy-to-use foundation for your next website, API, or cloud project

Vapor 22.4k Jan 3, 2023
libuv base Swift web HTTP server framework

Notice Trevi now open a Trevi Community. Yoseob/Trevi project split up into respective Trevi, lime, middlewares and sys packages at our community. If

leeyoseob 46 Jan 29, 2022
A Swift web framework and HTTP server.

A Swift Web Framework and HTTP Server Summary Kitura is a web framework and web server that is created for web services written in Swift. For more inf

Kitura 7.6k Dec 27, 2022
High Performance (nearly)100% Swift Web server supporting dynamic content.

Dynamo - Dynamic Swift Web Server Starting this project the intention was to code the simplest possible Web Server entirely in Swift. Unfortunately I

John Holdsworth 68 Jul 25, 2022
Reliable Server Side Swift ✭ Make Apache great again!

mod_swift mod_swift is a technology demo which shows how to write native modules for the Apache Web Server in the Swift 3 programming language. The de

The ApacheExpress Alliance 174 Oct 22, 2022