SwiftNIO: A simple guide to async on the server

An important topic in server-side Swift is asynchronous programming. This tutorial teaches you how to work with two important aspects of async programming: futures and promises, using SwiftNIO. By Joannis Orlandos.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

EventLoop-Specific Repositories

Before you create the routes, you need to create a factory method for the QuoteRepository because requests can run on different EventLoops. If the EventLoop changes while responding to a request, SwiftNIO will crash to prevent undefined behaviour from breaking the app.

Add the following code to QuoteResponder to set up the repository factory method:

// 1
let quoteRepository = ThreadSpecificVariable<QuoteRepository>()

func makeQuoteRepository(for request: HTTPRequest) -> QuoteRepository {
  // 2
  if let existingQuoteRepository = quoteRepository.currentValue {
    return existingQuoteRepository
  }

  // 3
  let newQuoteRepository = QuoteRepository(for: request.eventLoop)
  quoteRepository.currentValue = newQuoteRepository
  return newQuoteRepository
}

What’s going on here?

  1. A ThreadSpecificVariable holds the repository because the value will be different for each thread. Thanks to this, you don’t need to worry about thread safety.
  2. makeQuoteRepository(for:) returns a value unique to the current thread if one is available.
  3. Otherwise, a new QuoteRepository is created. The HTTPRequest is passed along so that its EventLoop can be used.

Now, you’re ready to write the first routes. To keep the code separated, each route will get its own method.

Fetching Quotes

First, add the listQuotes method to QuoteResponder, right below the last piece of code you added:

private func listQuotes(for request: HTTPRequest) -> EventLoopFuture<HTTPResponse> {
  // 1
  let repository = makeQuoteRepository(for: request)

  // 2
  return repository.fetchAllQuotes().thenThrowing { quotes in
    // 3
    let body = try HTTPBody(json: quotes, pretty: true)
    return HTTPResponse(status: .ok, body: body)
  }
}

Going through the above code:

  1. You uses a quote repository to fetch all the quotes needed, which you get using the factory method that you just created.
  2. Next, you use the repository’s fetchAllQuotes() method you added earlier.
  3. Once the promise has completed, you encode the returned quotes into a HTTPBody object as JSON using the thenThrowing method. You wrap the body in a HTTPResponse and send it back to the client.

The map function on a Future type transforms the future to a different type. The thenThrowing function on a future does the same, except it allows the transform function to throw an error.

In this case, the array of quotes is transformed into an HTTPResponse. The thenThrowing function is used because encoding an entity as JSON can throw an error.

Note: The web server used in this example project will emit a HTTP 500 Internal Server Error if any errors are thrown.

Creating New Quotes

The next route will let the client create new quotes. Add the following method to the QuoteResponder struct:

private func createQuote(from request: HTTPRequest) -> EventLoopFuture<HTTPResponse> {
  // 1
  guard let body = request.body else {
    return request.eventLoop.newFailedFuture(error: QuoteAPIError.badRequest)
  }

  do {
    // 2
    let quoteRequest = try body.decodeJSON(as: QuoteRequest.self)
    let quote = Quote(id: UUID(), text: quoteRequest.text)

    // 3
    let repository = makeQuoteRepository(for: request)

    // 4
    return repository.insert(quote).thenThrowing {
      let body = try HTTPBody(json: quote, pretty: true)
      return HTTPResponse(status: .ok, body: body)
    }
  } catch {
    // 5
    return request.eventLoop.newFailedFuture(error: error)
  }
}

Here is what is happening in this code:

  1. Confirm a body is present in the request. If there is no HTTPBody, there is no quote data to decode; therefore, you’ll throw an error.
  2. Next, you attempt to decode the JSON body into a Quote. This throws an error if the data is not JSON or is incorrectly formatted JSON.
  3. The method finds the repository for the current thread.
  4. Try to insert the new quote into the repository, returning the new quote in the HTTPResponse if it succeeded.
  5. Finally, the method throws an error if there was a problem with any of these steps.

Routing the Requests

To get everything up and running with more than just the static welcome message, replace the contents of the respond(to:) method in QuoteResponder with the following code:

// 1
switch request.head.method {
case .GET:
  // 2
  return listQuotes(for: request)
case .POST:
  // 3
  return createQuote(from: request)
default:
  // 4
  let notFound = HTTPResponse(status: .notFound, body: HTTPBody(text: "Not found"))
  return request.eventLoop.newSucceededFuture(result: notFound)
}

Breaking down this code:

  1. First, you switch on the method of the request.
  2. If this is a GET request, you call the listQuotes(for:) method to return all quotes as JSON.
  3. If its a POST request, you’ll instead call createQuote(from:) to insert a new quote.
  4. In all other cases, simply return a 404 Not Found HTTPResponse.

Stop the server running in Xcode and run it again to make it use your new methods.

Open RESTed and point it to http://localhost:8080/. Change the method to POST, rather than GET. The second table contains the body of the request. Add a key-value pair with the key text. Add your quote for the value and be sure to change the type from Form-encoded to JSON-encoded, otherwise you’ll get an Internal Server Error.

Press Send Request on the bottom right and you’ll see a response body returned with the quote you just submitted and an ID.

Sending request

To check if it worked, open the website at http://localhost:8080. A JSON array with the quote you just inserted will show up.

Request result

Great Scott! The futures worked, excellent work!

Take a moment to bask in the glory of success before you move on to the next section, where you’ll take the necessary steps to delete quotes.

Fetching One Quote

Before you can delete quotes, the repository needs to implement the ability to fetch a single quote since deleting a quote will find, delete and then return the deleted quote.

To do this, add the following method to QuoteRepository:

func fetchOne(by id: Quote.Identifier) -> EventLoopFuture<Quote?> {
  let promise = eventLoop.newPromise(of: Quote?.self)
  QuoteRepository.database.findOne(by: id, completing: promise)
  return promise.futureResult
}

This is similar to the fetchAllQuotes() method except for the fact you use the supplied id to call the database’s findOne(by:completing:) method. It returns a promise with a return type of an optional Quote, since the database may not contain a quote with this ID.

Next, add the following method to QuoteResponder:

private func getQuote(by id: String, for request: HTTPRequest)
  -> EventLoopFuture<HTTPResponse> {
    // 1
    guard let id = UUID(uuidString: id) else {
      return request.eventLoop.newFailedFuture(error: QuoteAPIError.invalidIdentifier)
    }

    // 2
    let repository = makeQuoteRepository(for: request)

    // 3
    return repository.fetchOne(by: id).thenThrowing { quote in
      // 4
      guard let quote = quote else {
        throw QuoteAPIError.notFound
      }

      // 5
      let body = try HTTPBody(json: quote, pretty: true)
      return HTTPResponse(status: .ok, body: body)
    }
}

This is what happens, here:

  1. You attempt to create a UUID from the received id and return an error if this is unsuccessful.
  2. Since the repository is needed to access the database, you get a QuoteRepository next.
  3. You attempt to fetch the from the repository using fetchOne(by:).
  4. If the quote doesn’t exist, you throw an error.
  5. Finally, you encode the quote as JSON and return it.