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?
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.