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 3 of 3 of this article. Click here to view the first page.

Implementing a findOne Route

To link up a findOne request into the respond method, you’ll need to replace the code for the GET case in the respond(to:) method implementation. Add the following code:

case .GET:
  // 1
  guard request.head.uri != "/" else {
    return listQuotes(for: request)
  }
  
  // 2
  let components = request.head.uri.split(separator: "/", maxSplits: .max, omittingEmptySubsequences: true)
  
  // 3
  guard let component = components.first,
        components.count == 1 else {
    return request.eventLoop.newFailedFuture(error: QuoteAPIError.notFound)
  }
  
  // 4
  let id = String(component)
  return getQuote(by: id, for: request)

Going through this step by step:

  1. If the route is the root path, use listQuotes(for:) to return all the quotes.
  2. Otherwise, split the request path into its components.
  3. Make sure that there is exactly one component or return an error.
  4. Find the quote by its identifier — the single component of the request path.

Try it out! Restart the app, create a quote and send a GET request to http://localhost:8080/uuid, replacing uuid with the ID of the quote you just inserted.

Deleting Quotes

Now, for erasing history! First, add this method to QuoteRepository:

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

Like in all the other methods in this class, you create a promise, call a database method providing the promise as its callback, and then return the promise’s future.

Next, add the following method to QuoteResponder:

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

    let repository = makeQuoteRepository(for: request)

    return repository.fetchOne(by: id).then { quote -> EventLoopFuture<HTTPResponse> in
      // 2
      guard let quote = quote else {
        return request.eventLoop.newFailedFuture(error: QuoteAPIError.notFound)
      }

      // 3
      return repository.deleteOne(by: id).thenThrowing {
        let body = try HTTPBody(json: quote, pretty: true)
        return HTTPResponse(status: .ok, body: body)
      }
    }
}

This is what you just wrote:

  1. As with getQuote(by:for:), you attempt to create a UUID from the supplied id.
  2. You try to retrieve the matching quote from the repository and if it doesn’t exist, return an error as an EventLoopFuture because then doesn’t allow throwing.
  3. Finally, you remove the quote and return the deleted quote as JSON.
Note: If you had used map instead of then, the result would be an EventLoopFuture<EventLoopFuture<HTTPResponse>>. Therefore, then is used to simplify the result to EventLoopFuture<HTTPResponse>.

Routing the Delete Requests

The final step for you is to create a route for the DELETE methods.

In the respond(to:) method, add this code above the default clause of the switch statement:

case .DELETE:
  // 1
  let components = request.head.uri.split(separator: "/", maxSplits: .max, omittingEmptySubsequences: true)
  
  // 2
  guard components.count == 1,
    let component = components.first else {
      return request.eventLoop.newFailedFuture(error: QuoteAPIError.notFound)
  }
  
  // 3
  let id = String(component)
  return deleteQuote(by: id, for: request)

This piece of code might look very familiar to your from the previous change you made to the GET case. Breaking this down:

  1. You split the path into its components.
  2. Check to ensure that there is exactly one component. That single component is used as the identifier of the removed quote.
  3. Finally, you send the delete request to the route that will return the response for the user.

Because there is no fully fledged web framework such as Kitura or Vapor in use here, the respond(to:) methods needs to route these requests manually.

To test this, restart the app, add some quotes and then use RESTed to call http://localhost:8080/uuid with a DELETE request, replacing uuid with the id of a quote you have added.

Delete request

Ta-da! Who would’ve imagined that erasing history is this easy? Take that, historians! :]

Where to Go From Here?

To learn more about SwiftNIO, have a look at our own tutorial on building a TCP server with SwiftNIO that dives deeper into event loops and networking. If you’re willing to dive deeper into the framework yourself, check out Apple’s SwiftNIO documentation on GitHub.

I hope this tutorial was useful for you. Feel free to join the discussion below!