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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SwiftNIO: A simple guide to async on the server
20 mins
- The Event Loop
- Futures and Promises
- Getting Started
- The Quotes Repository
- Setting up the Quotes Repository
- EventLoop-Specific Repositories
- Fetching Quotes
- Creating New Quotes
- Routing the Requests
- Fetching One Quote
- Implementing a findOne Route
- Deleting Quotes
- Routing the Delete Requests
- Where to Go From Here?
EventLoop-Specific Repositories
Before you create the routes, you need to create a factory method for the QuoteRepository
because requests can run on different EventLoop
s. 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?
- 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. -
makeQuoteRepository(for:)
returns a value unique to the current thread if one is available. - Otherwise, a new
QuoteRepository
is created. TheHTTPRequest
is passed along so that itsEventLoop
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:
- You uses a quote repository to fetch all the quotes needed, which you get using the factory method that you just created.
- Next, you use the repository’s
fetchAllQuotes()
method you added earlier. - Once the promise has completed, you encode the returned quotes into a
HTTPBody
object as JSON using thethenThrowing
method. You wrap the body in aHTTPResponse
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.
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:
- 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. - 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. - The method finds the repository for the current thread.
- Try to insert the new quote into the repository, returning the new quote in the
HTTPResponse
if it succeeded. - 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:
- First, you switch on the method of the request.
- If this is a
GET
request, you call thelistQuotes(for:)
method to return all quotes as JSON. - If its a
POST
request, you’ll instead callcreateQuote(from:)
to insert a new quote. - 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.
To check if it worked, open the website at http://localhost:8080. A JSON array with the quote you just inserted will show up.
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:
- You attempt to create a
UUID
from the receivedid
and return an error if this is unsuccessful. - Since the repository is needed to access the database, you get a
QuoteRepository
next. - You attempt to fetch the from the repository using
fetchOne(by:)
. - If the quote doesn’t exist, you throw an error.
- Finally, you encode the quote as JSON and return it.