Home · Server-Side Swift Tutorials

SwiftNIO Tutorial: Practical Guide for Asynchronous Problems

In this tutorial, you’ll solve common asynchronous problems about promises and futures in SwiftNIO by building a quotations app.

4.8/5 4 Ratings

Version

  • Swift 5, macOS 10.15, Xcode 11

Futures and promises abstract the asynchronous creation and usage of a single value. They’re critical to creating services in SwiftNIO because they make asynchronous code more readable. However, code can become more complicated when you add a layer of abstraction.

In this SwiftNIO asynchronous tutorial, you’ll learn how to use futures and promises while still keeping your code simple and concise.

To do this, you’ll build a simple SwiftNIO service called Quotanizer, which can store quotes or serve random ones, ensuring that every quote will only appear once. In the process, you’ll learn:

  • How to hop event loops and use pipeline handlers.
  • How to solve common problems using promises and futures.
  • Methods that make futures easier to use.
Note: If you’re not familiar with SwiftNIO, async programming, or futures and promises, read our tutorial, A Simple Guide to Async on the Server before you begin.

Getting Started

To start this tutorial, download the starter project by clicking the Download Materials button at the top or bottom of this tutorial.

You’ll also need Xcode 11 or later and an HTTP client such as RESTed.

The materials archive contains a project in the starter folder, consisting of an HTTP server and a mocked database. This will get you started.

The final folder contains the same project, but after applying all the changes described in this tutorial — useful in case you want to compare it with the result of your work.

To begin, navigate to starter and double-click Package.swift to open it.

Xcode appears, and you’re ready to go! It automatically downloads all dependencies that the project needs — which, in this case, is only the SwiftNIO framework. All the code is in Sources/PromisesFutures, so be sure to expand this folder in the Project navigator.

Group containing the source code

Creating the Web Service

As mentioned before, you’ll start by creating the Quotanizer web service to server and store various quotes. This service will allow adding a quote using a HTTP POST request, and fetching a random quote and removing it from the database using a HTTP GET request.

All SwiftNIO applications depend on an event loop for network communication, which is basically an infinite loop getting “interrupted” by various events, such as network requests. You can learn more about the event loop in A Simple Guide to Async on the Server.

To create a web service, first create a MultiThreadedEventLoopGroup instance by adding the following line to your now-empty main.swift:

let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

This creates as many event loops as there are CPU cores. This works well if you need to run a service with as much performance as is available. It’s a good solution for apps that need to handle a lot of load, as web services do.

You can set the numberOfThreads to a smaller number if you run a small service off the event loop. One event loop is enough for many tasks, like database drivers.

Bootstrapping an HTTP Service

To create an HTTP service in SwiftNIO, you must first create a ServerBootstrap. This bootstrap functions as a template for a server socket.

An EventLoop, selected by a round-robin approach, pairs with each new client. This balances clients over the available threads so that, on average, every thread handles the same number of clients.

To create a bootstrap for the HTTP service, add the following code to main.swift:

let bootstrap = ServerBootstrap(group: group)
  // 1
  .serverChannelOption(ChannelOptions.backlog, value: 256)
  // 2
  .serverChannelOption(ChannelOptions.socket(
    SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
  // 3
  .childChannelInitializer { channel in
    channel.pipeline.configureHTTPServerPipeline(
      withErrorHandling: true).flatMap {

      channel.pipeline.addHandler(HTTPHandler(responder: QuoteResponder()))
    }
  }

The code above:

  1. Specifies that you can have up to 256 waiting clients.
  2. Enables reusing the IP address and port so that many threads can receive clients.
  3. Configures each client socket to communicate over HTTP. After that, adds a handler to respond to HTTP requests.

Starting the Service

To use the HTTP service, you need to choose a host and port to publish it on. HTTP’s default port is 80 for unencrypted connections and 443 for encrypted connections. Note that these ports are below 1024, so you need administrative privileges to use them.

Most services will publish themselves on the host 0.0.0.0 or ::1. These addresses will publish the bootstrap on any address linked to the computer.

Still in main.swift, add the following code, and your service is ready to go!

// 1
let host = "::1"
let port = 1111

// 2
let serverChannel = try bootstrap.bind(host: host, port: port).wait()

// 3
guard serverChannel.localAddress != nil else {
  fatalError("Unable to bind to \(host) at port \(port)")
}

// 4
print("Server is running on http://localhost:1111")
try serverChannel.closeFuture.wait()

The code above consists of four steps, where you:

  1. Choose a host that allows anyone to connect to this service if they visit on the selected port.
  2. Bind a socket to this host and port, making it available.
  3. Check if the service is now available and stop the app if binding failed.
  4. Wait for the server socket to shut down. This keeps the app running.
Note: If you don’t .wait() for the server to close, the app will shut down.

Build and run the project and take a look at your Debug Console, where you should see Server is running on http://localhost:1111. Then, go to http://localhost:1111 in a browser. You should see a message saying server error. If both of these tests work well for you, it means your server is good to go!

You might’ve noticed the code above uses methods such as serverChannelOption and childChannelInitializer, as well as the fact the call to bootstrap.bind(host:port:).wait() returns a Channel object. But what is a channel?

Using Channels

In SwiftNIO, channels wrap each socket. Channels are types that can read and write data. Sockets are the most common type of channels.

Each channel has a pipeline consisting of handlers. Each handler transforms either inbound or outbound traffic. Inbound handlers receive data from the channel, usually a socket, that transforms it.

Inbound handlers work from front to back. Each step in the process is a transformation on the output of the previous data. The arrival of new data from a channel initializes these pipelines.

HTTP inbound handlers

Outbound handlers work from back to front, then send the result to the channel. The TCP channel provided by SwiftNIO requires you to send a ByteBuffer. The input that SwiftNIO’s TCP channel receives is also a ByteBuffer.

HTTP outbound handlers

Now that you have your web service set up, it’s time to start using quotes with Quotanizer.

The Quote Repository

The starter project has already set up the basic routes so that Quotanizer can insert quotes into the repository and retrieve them again.

The app should get the quotes from QuoteRepository, but you haven’t implemented the repository’s functionality yet.

Your next step is to provide the code to insert a new quote and to retrieve a random quote.

Adding Quotes to Quotanizer

Repositories are an abstraction of interactions with data and databases. Many developers combine the repository pattern with dependency injection, which enables you to write tests for individual pieces of code.

In QuoteRepository.swift replace the insert(_:) implementation with the following:

func insert(_ quote: Quote) -> EventLoopFuture<Void> {
  // 1
  do {
    // 2
    let json = try JSONEncoder().encode(quote)
    let entity = DataEntity(data: json)
    // 3
    return QuoteRepository.database.addEntity(entity).hop(to: eventLoop)
  } catch {
    // 4
    return eventLoop.makeFailedFuture(error)
  }
}

Now, break down what’s happening above:

  1. A do-catch block catches errors thrown from encoding the quote.
  2. Encode the quote as JSON.
  3. Insert the quote in the database and return the result on the repository’s event loop.
  4. Return a failed EventLoopFuture containing the JSONEncoder’s error, if an error arises.

Instead of throwing errors, this code wraps the caught errors in an EventLoopFuture, which already uses a success and failure state. Returning an EventLoopFuture and throwing errors from the same place is an anti-pattern.

Every EventLoopFuture relies on an EventLoop. On completion of the future, handlers such as map and flatMap run on the event loop’s thread. For this reason, hopping to the correct event loop is important. If SwiftNIO ends ups on the wrong event loop, unexpected behavior may occur.

During development, these programming errors will crash the app. But if you’re on a release build, you have to be even more careful. SwiftNIO will not check for programming errors, so they can go unseen for a long time.

Completing the Repository

Finally, to complete the repository, implement findAndDeleteOne():

func findAndDeleteOne() -> EventLoopFuture<Quote?> {
  // 1
  return QuoteRepository.database.getAllEntities().flatMap { entities in
    // 2
    guard let entity = entities.randomElement() else {
      // 3
      return self.eventLoop.makeSucceededFuture(nil)
    }

    // 4
    return QuoteRepository.database.deleteOne(by: entity.id).flatMapThrowing {
      // 5
      return try JSONDecoder().decode(Quote.self, from: entity.data)
    }
    // 6
  }.hop(to: eventLoop)
}

Here’s what the code above does:

  1. Reads all quotes from the database.
  2. Selects a random quote.
  3. Returns nil in an EventLoopFuture if there are no quotes.
  4. Removes the quote from the database, to ensure that it’s only returned once.
  5. Decodes the quote from JSON.
  6. Finally, the resulting EventLoopFuture hops to the repository’s event loop.

Quotanizer should be working now, but is it really? Your next step is to test the app to ensure there you haven’t overlooked any errors.

Testing the App

Build and run the app and visit http://localhost:1111. Make sure your target is My Mac.

Selecting the correct target

When visiting the website, you’ll see a server error. The route expects to receive a quote from the repository, but the database has no quotes yet!

Open RESTed, or a different HTTP client of your choice, and make a POST request with the URL http://localhost:1111.

Add a parameter with the name text, then add any quote you like and make sure the request is JSON-encoded. Next, run the request by clicking Send Request. You’ll see the response in the panel on the right.

POST request on RESTed

Now, you can visit the URL from your web browser: http://localhost:1111 again. The quote you inserted will appear.

If you refresh, however, you’ll see another server error. That’s because when the app serves a quote, it then removes it, so the database is empty again.

Showing the error when the database is empty is not the way you want the app to work. You’ll use helpers to fix that error in the next section!

Creating Helpers for Futures

Helpers are great tools for common tasks such as unwrapping optionals. When you write extensions in Swift, futures become more powerful and code less cluttered and error-prone, as well as more readable.

If you use Vapor, you can leverage its helpers. The framework provides helpers for most use cases on the web. But for this app, there are no helpers other than the ones SwiftNIO provides.

Open QuoteResponder.swift and, in respond(to:), look at the GET case:

case (.GET, "/"):
  return getQuote(for: request)

It doesn’t implement any error handling, which is why you see a server error in the browser when there’s no quote to retrieve.

To fix this, add error handling by replacing that case with the following code:

case (.GET, "/"):
  return getQuote(for: request).flatMapErrorThrowing { error in
    guard case QuoteAPIError.notFound = error else {
      throw error
    }

    return HTTPResponse(status: .notFound, body: nil)
  }

flatMapErrorThrowing(_:) transforms errors into failure cases. Here, you check if the error is QuoteAPIError.notFound, in which case you respond with the Not Found status, or status code 404 — and that fixes the issue.

Build and run the project again, and go to http://localhost:1111 one final time. You should see an actual HTTP 404 Not Found error, as expected.

Unwrapping Variables

Your service is functioning now, but your error handling can be prettified quite a bit.

Look at getQuote(for:) and you’ll see regular optional unwrapping. As your project grows, you’ll unwrap a lot of variables. To make optional unwrapping less verbose, add the following extension in QuoteResponser.swift:

extension EventLoopFuture {
  func unwrapOptional<Wrapped>(orThrow error: Error)
    -> EventLoopFuture<Wrapped> where Value == Wrapped? {
    flatMapThrowing { optional in
      guard let wrapped = optional else {
        throw error
      }

      return wrapped
    }
  }
}

This adds a generic helper to EventLoopFuture, which will indirectly return either an instance of the generic type Wrapped or an error. The future’s Value must be an optional wrapping an instance of Wrapped.

This gives you the power to rewrite getQuote(for:) as such:

private func getQuote(for request: HTTPRequest)
  -> EventLoopFuture<HTTPResponse> {
  let repository = QuoteRepository(eventLoop: request.eventLoop)

  return repository.findAndDeleteOne()
    .unwrapOptional(orThrow: QuoteAPIError.notFound)
    .flatMapThrowing { quote in
      let body = try HTTPBody(json: quote, pretty: true)
      return HTTPResponse(status: .ok, body: body)
    }
}

This change doesn’t add anything functional, but it improves the code a bit. Take a look at the original code:

return repository
  .findAndDeleteOne()
  // 1
  .flatMapThrowing { quote in
    guard let quote = quote else {
      throw QuoteAPIError.notFound
    }
    ...
  }

It uses an explicit optional binding in a guard statement, throwing an error if unwrapping fails. Compare it with the improved version:

return repository
  .findAndDeleteOne()
  // 1
  .unwrapOptional(orThrow: QuoteAPIError.notFound)
  .flatMapThrowing { quote in
    ...
  }

Here, you delegate the unwrapping to unwrapOptional(orThrow:), which takes care of throwing the passed error if unwrapping fails. It’s easier to read, and also less ambiguous to write.

Where to Go From Here?

Download the final project by using the Download Materials button at the top or bottom of this page.

To learn more about networking with SwiftNIO, read our tutorial, TCP Server With the SwiftNIO Networking Framework.

If you want to learn more about developing web services, our Server-Side Swift With Vapor book is a good starting place.

Generics are important when creating helpers. To learn more about generics, check out our Swift Generics Tutorial: Getting Started.

SwiftNIO is a remarkable framework. Are you excited about Swift on the server? Feel free to share your thoughts on Swift and SwiftNIO in the discussions below!

Average Rating

4.8/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments