Getting Started With Server-Side Swift and Amazon Smoke
Do you find yourself wanting to leverage your Swift skills on the backend and don’t know where to start? In this tutorial, you’ll build a REST API using Server-Side Swift and Amazon Smoke. By Jonathan S Wong.
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
Getting Started With Server-Side Swift and Amazon Smoke
20 mins
- Getting Started
- Smoke and RESTful API Routing
- Creating a GET Topic Operation
- Creating the Response
- Adding Topics
- Adding Route Handlers
- Defining Errors
- Specifying Operations
- Conforming to Smoke Protocols
- Configuring the Application Server
- Running Your Server
- Creating a POST Topic Operation
- Adding a Response Object
- Combining the Request and Response Objects
- Adding a Route
- Validating Your Data
- Troubleshooting
- Where to Go From Here?
Do you find yourself wanting to leverage your Swift skills on the backend and don’t know where to start? If you need a lightweight way to create a REST API backed by one of the most influential and innovative companies in the world, give Amazon Smoke a try. It’s built on top of SwiftNIO, like Vapor and Kitura.
By leveraging Swift and the power of protocols, you’ll see how easy it is to get up and running with Smoke.
In this tutorial, you’ll use Server-Side Swift and Amazon Smoke to build the Level Up REST API, which helps you track topics to learn to progress your career on this never-ending journey as a software developer. You will learn how to configure a Smoke server to accept GET and POST requests and how to respond to these requests.
Getting Started
Start by downloading the project materials using the Download Materials button at the top or bottom of this tutorial. Then open Package.swift in the starter folder.
Xcode will pull down the required dependencies for this tutorial: the Smoke framework and its dependencies.
The starter project includes a Topic
model that you’ll use to keep track of what you’re studying, along with the amount of time you plan to study it. A TopicStore
is used to keep track of the various topics. Finally, An ApplicationContext
is used in each of the input requests to the server and is also included in the starter project. This is where the TopicStore gets initialized.
Before you start working on your server code, it’s time for you to learn some of the basics of Smoke and REST.
Smoke and RESTful API Routing
REST is an acronym for Representational State Transfer. In RESTful apps, each unique URL represents a resource. In this tutorial, you’ll have one such resource — Topic, which you’ll use with RESTful verbs like GET to fetch topics and POST to add new topics.
In Smoke, you provide various operation functions to handle the RESTful verbs. Smoke decodes the request into the operation’s input along with the ApplicationContext
and encodes the response in the operation’s output.
You can think of operations as functions you use to set up the requests and responses in a REST API.
The ApplicationContext
is created when the server starts up and is passed to each operation handler.
Creating a GET Topic Operation
Create a new Swift file called TopicsGetRequest.swift in the LevelUp group. Replace its content with the following:
import SmokeHTTP1
import SmokeOperations
import SmokeOperationsHTTP1
import SmokeOperationsHTTP1Server
// 1
func topicsGetOperation(
input: TopicsGetRequest,
context: ApplicationContext
) throws -> TopicsGetResponse {
// 2
TopicsGetResponse(topics: context.topicStore.topics)
}
// 3
public struct TopicsGetRequest: Codable, Validatable, Equatable {
// 4
public func validate() throws {}
}
Don’t worry about the resulting compilation error, you’ll deal with it momentarily.
In the code you just added, you:
- Add an operation to handle the GET request for a topic. The input is of type
TopicsGetRequest
, andApplicationContext
is also passed to the operation. The return type isTopicsGetResponse
, but you haven’t created this yet. This means you can’t build right now — but you’ll fix that in a moment. - Return the response for the operation, which is of type
TopicsGetResponse
. In the response, you useApplicationContext
to fetch thetopics
from thetopicStore
. You’ll create both of these in a moment. - Define
TopicsGetRequest
. This type conforms toCodable
,Validatable
andEquatable
. You’re required to use theValidatable
protocol for input requests and output response types. - Add an empty
validate()
, to satisfy theValidatable
protocol conformance.
Now that you have the request, it’s time to create the response.
Creating the Response
Create a new Swift file called TopicsGetResponse.swift with the following code:
import SmokeHTTP1
import SmokeOperations
import SmokeOperationsHTTP1
import SmokeOperationsHTTP1Server
// 1
public struct TopicsGetResponse: Codable, Validatable, Equatable {
// 2
public var topics: [Topic]
public init(topics: [Topic]) {
self.topics = topics
}
// 3
public func validate() throws {}
}
This should look similar to TopicsGetRequest
above. You:
- Define
TopicsGetResponse
. LikeTopicsGetRequest
, this type conforms toCodable
,Validatable
andEquatable
. - Define an array of
Topic
s, which the response will return. - Add an empty
validate()
to satisfy theValidatable
protocol requirement.
Adding Topics
There’s one last step you need to do before you can build successfully. What good is a TopicStore
if you can’t add any Topic
s to it? You’ll fix that next.
In ApplicationContext.swift, add this property to TopicStore
:
var topics: [Topic] = []
Then, add the following line to addTopic(_:)
:
topics.append(topic)
These two lines of code create an array to hold all the interesting topics you intend to learn about. They then configure addTopic
to add topics to add to this array.
Time to build the work you’ve done so far. Make sure to select My Mac in the device pop-up and press Command-B. A successful build tells you that everything’s set up correctly so far, but you aren’t ready to run the server yet.
Adding Route Handlers
Now that you’ve created your first operation, it’s time to define which operation gets run for the incoming requests.
Start by creating a new Swift file called ModelOperations.swift. Add the following code to it:
import SmokeOperations
import SmokeOperationsHTTP1
// 1
public enum ModelOperations: String, Hashable, OperationIdentity {
// 2
case topicGet
// 3
public var operationPath: String {
switch self {
case .topicGet:
return "/topics"
}
}
}
extension ModelOperations: CustomStringConvertible {
// 4
public var description: String { rawValue }
}
- To add the operation for a route, you need to define a type that conforms to
OperationIdentity
. - You create a
case
for each of your routes. Since you only have one route so far, you create a single route to GET topics. - The
OperationIdentity
protocol requires anoperationPath
property. This is where you define the GET topics route as aString
. - Lastly, you implement the description property to satisfy the
CustomStringConvertible
protocol. This is in a separate extension to keep the code neat.
Defining Errors
To add an operation, you need to define the errors your route will handle. Add the following below ModelOperations
:
// 1
public struct ErrorTypes: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
// 2
public static let serverError = ErrorTypes(rawValue: 1)
}
// 3
extension ErrorTypes: ErrorIdentifiableByDescription {
public var description: String {
switch rawValue {
case 1:
return "serverError"
default:
return ""
}
}
}
Here’s what you’re doing in the code above:
- You define a struct called
ErrorTypes
to represent any errors that occur in your REST API. - This makes it easy to create an
ErrorType
of typeserverError
. - Errors must conform to
ErrorIdentifiableByDescription
, which is a typealias for aSwift.Error
andCustomStringConvertible
. This defines your error description.
You’re using an OptionSet
here instead of an enum
because even though there’s only one ErrorType
defined, you can imagine that multiple internal errors may happen in your application and you only want to expose a single ErrorType
to a consumer. For example, if you had two errors, invalidUsername
and invalidPassword
, you would only want to expose a generic error like invalidCredentials
to the client from your application.
Specifying Operations
Once you create your routes, you need to specify which operation to use for which routes.
Do this by adding the following below ErrorTypes
, which you just created:
// 1
public func addOperations<SelectorType: SmokeHTTP1HandlerSelector>(
selector: inout SelectorType
) where SelectorType.ContextType == ApplicationContext,
SelectorType.OperationIdentifer == ModelOperations {
// 2
selector.addHandlerForOperation(.topicGet,
httpMethod: .GET,
operation: topicsGetOperation,
allowedErrors: [(ErrorTypes.serverError, 500)])
}
Here’s a walk-through:
- This function specifies which operation executes for each individual route.
SmokeHTTP1HandlerSelector
restricts the handler to an operation type using the HTTP1 protocol. It also has an associated type requirements for itsContextType
andOperationIdentifier
, which you set to yourApplicationContext
andModelOperations
. - Using the provided
selector
, you invokeaddHandlerForOperation(_:httpMethod:operation: allowedErrors)
.- This takes in your
.topicGet
route. - Since this is a GET method, you use the
.GET
HTTP method. - You pass in
topicsGetOperation
as the operation that handles this request. - Lastly, you pass the error types this route can throw.
- This takes in your
Conforming to Smoke Protocols
At this point, your build isn’t passing again. Smoke leverages Swift protocols to ensure that your types satisfy the type requirements it expects.
In this case, TopicsGetRequest
and TopicsGetResponse
need to conform to OperationHTTP1InputProtocol
and OperationHTTP1OutputProtocol
respectively. You’ll fix that next.
Add the following code at the bottom of TopicsGetRequest.swift:
extension TopicsGetRequest: OperationHTTP1InputProtocol {
public static func compose(queryDecodableProvider: () throws -> TopicsGetRequest,
pathDecodableProvider: () throws -> TopicsGetRequest,
bodyDecodableProvider: () throws -> TopicsGetRequest,
headersDecodableProvider: () throws -> TopicsGetRequest)
throws -> TopicsGetRequest {
try queryDecodableProvider()
}
}
OperationHTTP1InputProtocol
is a protocol that represents the input to an operation for an HTTP request. It defines associatedtype
requirements for how the operation will be used and decoded. In your case, your TopicsGetRequest
is Codable
and it’s the type that is to be decoded.
Similarly, add the following protocol conformance to TopicsGetResponse.swift:
// 1
extension TopicsGetResponse: OperationHTTP1OutputProtocol {
// 2
public var bodyEncodable: TopicsGetResponse? { self }
// 3
public var additionalHeadersEncodable: TopicsGetResponse? { nil }
}
Here’s the breakdown of the above code:
-
OperationHTTP1OutputProtocol
is a protocol that represents the output from an operation for an HTTP response. - You define what you’ll encode in the body of the response, which is
TopicsGetResponse
. - Since you don’t need any additional headers, you return
nil
foradditionalHeadersEncodable
.
Success! Build once again, and you’ll see everything builds with no compilation errors.
You’re almost ready to finally run your server. The last thing you need to do is to configure your application server.
Configuring the Application Server
Time to set up the server so that when you finally get to build and run, the server knows what to do with any requests that it receives.
In main.swift, add the following:
import SmokeOperationsHTTP1
import SmokeOperationsHTTP1Server
import AsyncHTTPClient
import NIO
import SmokeHTTP1
// 1
struct LevelUpInvocationContextInitializer: SmokeServerPerInvocationContextInitializer {
// 2
typealias SelectorType =
StandardSmokeHTTP1HandlerSelector<ApplicationContext,
MyOperationDelegate,
ModelOperations>
let handlerSelector: SelectorType
// 3
let applicationContext = ApplicationContext()
}
// 4
typealias MyOperationDelegate =
JSONPayloadHTTP1OperationDelegate<SmokeInvocationTraceContext>
Looking at the code above:
- To start the HTTP1 server, Smoke requires a type that conforms to
SmokeServerPerInvocationContextInitializer
. - You then define a
SelectorType
type alias to pick a handler based on the URI and HTTP method of the incoming request. Notice it uses bothApplicationContext
andModelOperations
, which you created earlier, as well as a new type that you’ll learn about in a moment. - You create an
ApplicationContext
that will be passed to each of the operations. - Smoke has a convenience type alias to delegate any special encoding and decoding to an operation delegate. The framework also provides a default
JSONPayloadHTTP1OperationDelegate
that expects JSON requests and responses, which your REST API uses. TheSmokeInvocationTraceContext
provides some basic tracing of your request and response headers.
Now, add the following to the end of LevelUpInvocationContextInitializer
:
// 1
init(eventLoop: EventLoop) throws {
var selector = SelectorType(
defaultOperationDelegate: JSONPayloadHTTP1OperationDelegate()
)
addOperations(selector: &selector)
self.handlerSelector = selector
}
// 2
public func getInvocationContext(
invocationReporting: SmokeServerInvocationReporting<SmokeInvocationTraceContext>)
-> ApplicationContext {
applicationContext
}
// 3
func onShutdown() throws {}
Here’s what this code does:
- When initializing this class, you use SwiftNIO’s
EventLoop
to create an instance ofSelectorType
and add the operations to your server. - Smoke uses
getInvocationContext(invocationReporting:)
when a new request comes in. From it, you return the same instance ofapplicationContext
. If you created a new instance here, you’d never persist any of your awesome topics! - If you had anything to do on server shutdown, like cleaning up resources, you’d do so here. For this tutorial, leave this blank.
Finally, add at the bottom of the file:
SmokeHTTP1Server.runAsOperationServer(LevelUpInvocationContextInitializer.init)
This will start your server on port 8080.
Give it a go! Build and run.
The Xcode console will show an entry like this to tell you that the server is up and running on port 8080.
2020-07-18T10:01:33+1000 info: SmokeHTTP1Server started on port 8080.
Running Your Server
Now that your server is running, use your favorite REST client to test it out. This tutorial uses the free version of Insomnia.
Set up the request as follows:
- URL: http://localhost:8080/topics
- Method: GET
You’ll see an empty list of topics returned, which isn’t too interesting.
You still need a way to add topics before this can help you level up your skills. It’s time to level up this API and add the ability to add new topics.
Creating a POST Topic Operation
Create a new Swift file called TopicsPostRequest.swift and add the following code to it:
import SmokeHTTP1
import SmokeOperations
import SmokeOperationsHTTP1
import SmokeOperationsHTTP1Server
// 1
public struct TopicsPostRequest: Codable, Validatable, Equatable {
// 2
public var name: String
public var duration: Int
public func validate() throws {}
}
// 3
extension TopicsPostRequest: OperationHTTP1InputProtocol {
public static func compose(
queryDecodableProvider: () throws -> TopicsPostRequest,
pathDecodableProvider: () throws -> TopicsPostRequest,
bodyDecodableProvider: () throws -> TopicsPostRequest,
headersDecodableProvider: () throws -> TopicsPostRequest
) throws -> TopicsPostRequest {
try bodyDecodableProvider()
}
}
This should look familiar to you by now. In the above code:
- You create a
TopicsPostRequest
. - This request has two properties: the
name
of the topic you want to create and theduration
of time you’ll spend learning this hot topic. - Conform to Smoke’s
OperationHTTP1InputProtocol
.
Adding a Response Object
Next, you need to add the response object. Create a new Swift file called TopicsPostResponse.swift and replace its content with:
import SmokeHTTP1
import SmokeOperations
import SmokeOperationsHTTP1
import SmokeOperationsHTTP1Server
// 1
public struct TopicsPostResponse: Codable, Validatable, Equatable {
public func validate() throws {}
}
// 2
extension TopicsPostResponse: OperationHTTP1OutputProtocol {
public var bodyEncodable: TopicsPostResponse? { self }
public var additionalHeadersEncodable: TopicsPostResponse? { nil }
}
In this code, you:
- Create a new
TopicsPostResponse
to handle the response to POSTing a topic. - Conform to Smoke’s
OperationHTTP1OutputProtocol
, as you did inTopicsGetResponse
.
Combining the Request and Response Objects
Now that you’ve added your request and response objects, add this function at the top of TopicsPostRequest.swift to bind them together.
// 1
func topicsPostOperation(
input: TopicsPostRequest,
context: ApplicationContext
) throws -> TopicsPostResponse {
// 2
let topic = Topic(name: input.name, duration: input.duration)
// 3
context.topicStore.addTopic(topic)
// 4
return TopicsPostResponse()
}
Here’s the walk-through:
- You create an operation that takes
TopicsPostRequest
along withApplicationContext
as input and returnsTopicsPostResponse
as output. - You create a new topic with the
name
andduration
passed in from the request. - Now that you’ve created the new topic, you add it to the
topicStore
. - Return a new
TopicsPostResponse
.
Adding a Route
The next task is to add a route for your new request and response. In ModelOperations.swift, add the following case to ModelOperations
:
case topicPost
Also, in operationPath
, replace:
case .topicGet:
With:
case .topicGet, .topicPost:
To make a POST request to create a topic at the /topics
route, you still need to tell Smoke which operation to use. Still in ModelOperations.swift, add the following to the bottom of addOperations(selector:)
:
selector.addHandlerForOperation(.topicPost,
httpMethod: .POST,
operation: topicsPostOperation,
allowedErrors: [(ErrorTypes.serverError, 500)])
With all of that done — time to build and run.
In Insomnia or another REST client of your choice, create a POST request as follows:
- URL: http://localhost:8080/topics.
- Method: POST.
- Select JSON as the request’s content type.
- Add a parameter
name
for the name of your topic and a parameterduration
for the amount of time you’ll spend learning the topic.
Success! Now you can create new topics in your REST API. Try creating a few and then use the same GET request as before to fetch your topics, which will show you the entries you just created.
ApplicationContext
, and therefore TopicStore
, are re-created each time you restart your server. Topics don’t persist between launches. When implementing your own server, you may choose any kind of persistence mechanism or database to solve this.
Validating Your Data
Validation is as important in back-end development as it is in an iOS app. All requests require that your type conforms to Validatable
. You haven’t implemented any validations yet, so users can add invalid data, like negative durations or topics with no name.
In TopicsPostRequest.swift, add the following in validate()
:
// 1
guard !name.isEmpty else {
throw SmokeOperationsError.validationError(
reason: "Name cannot be empty."
)
}
// 2
guard duration > 0 else {
throw SmokeOperationsError.validationError(
reason: "Duration must be positive."
)
}
- If the name is empty, you throw a
validationError
stating that the name can’t be empty. - Similarly, if the duration isn’t greater than 0, you throw a
validationError
because the duration must be positive.
Build and run again.
This time, in Insomnia or your REST client of choice, pass an empty name in your POST request as follows:
You’ll now see your error message on the right!
Now fix the name parameter, but put in a negative duration as follows:
Awesome! You’ve added error conditions just by implementing the Validatable
protocol!
Troubleshooting
The experience of writing server apps on Xcode still hasn’t caught up to building an iOS app. If you’re getting build errors, try cleaning your project by pressing Shift-Cmd-K and then rebuild your server.
Where to Go From Here?
Download the completed project for this tutorial using the Download Materials button at the top or bottom of this page.
If you want to learn more about Smoke and investigate its sample code, visit the Smoke Framework Git Repo. And see how Amazon is using Smoke in their Server-Side Swift talk.
For an article to help you get started using Server-Side Swift, check out Getting Started with Server-Side Swift with Vapor 4 or check out the awesome book, Server-Side Swift with Vapor.
And for more about working with SwiftNIO, read TCP Server With the SwiftNIO Networking Framework.
I hope you’ve enjoyed this article on using Amazon Smoke with Server-Side Swift. If you have any questions, feel free to leave them in the discussion forum below.
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more