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?
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:
- If the route is the root path, use
listQuotes(for:)
to return all the quotes. - Otherwise, split the request path into its components.
- Make sure that there is exactly one component or return an error.
- 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:
- As with
getQuote(by:for:)
, you attempt to create aUUID
from the suppliedid
. - You try to retrieve the matching quote from the repository and if it doesn’t exist, return an error as an
EventLoopFuture
becausethen
doesn’t allow throwing. - Finally, you remove the quote and return the deleted quote as JSON.
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:
- You split the path into its components.
- Check to ensure that there is exactly one component. That single component is used as the identifier of the removed quote.
- 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.
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!