Getting Started With HTTP Middleware in Kitura
Middleware is a popular way to to handle incoming server-side requests and outgoing responses. In this tutorial, you’ll learn how to add middleware to a REST API that’s built in Swift and uses the Kitura framework. You’ll learn how to add CORS (Cross-Origin Resource Sharing) policies, custom logic and authentication to the routes of an app that knows the meaning of life. By David Okun.
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 HTTP Middleware in Kitura
25 mins
Prepping Your Kitura Server for Middleware
In Xcode, navigate to your Package.swift and the following code to the end of the dependencies
array:
.package(url: "https://github.com/IBM-Swift/Kitura-CORS.git", .upToNextMinor(from: "2.1.0")),
.package(url: "https://github.com/IBM-Swift/Kitura-CredentialsHTTP.git", .upToNextMinor(from: "2.1.3"))
Next, scroll down to targets
and add the following two dependencies to your Application
target:
"KituraCORS", "CredentialsHTTP"
Close your Xcode project. Open Terminal and navigate to the root directory of your project. Run two commands:
swift package generate-xcodeproj
xed .
You have now updated your project to add CORS middleware and some authentication capabilities to your project.
Next, you’ll write three HTTP routes that fail or give you an undesirable result at first, and then you will write middleware to make each of them work correctly!
In Xcode, open the Sources/Application/Routes directory in the Project Navigator on the left, and right-click on the directory. Click on New File…, and add a new Swift file called RazeRoutes.swift. Make sure you select the Application
target.
Replace the contents of the file with the following import statements and initialization function:
import LoggerAPI
import KituraContracts
import Kitura
func initializeRazeRoutes(app: App) {
}
Before you start adding some more code to this file, go back to Application.swift and add the following line to the end of postInit()
:
initializeRazeRoutes(app: self)
Every HTTP route you now add in RazeRoutes.swift will register with your router every time you start your server. Now, you’ll add a place to put all of your middleware.
Right-click Application.swift and click New File… again. This file should be named Middleware.swift, and should also be targeted to Application
.
Replace the file’s contents with the following:
import Foundation
import Kitura
import KituraCORS
class Middleware {
}
Alright, the stage is set — time for you to enable cross-origin requests on your server and get your first taste of middleware!
Enabling CORS
Open RazeRoutes.swift and add this route registration to initalizeRazeRoutes
:
app.router.get("/cors", handler: corsHandler)
Xcode should show you an error at this point because you have not yet declared a function called corsHandler
. Fix that by adding the following code at the very bottom of the file outside of your function:
// 1
func corsHandler(request: RouterRequest, response: RouterResponse, next: () -> Void) {
// 2
guard response.headers["Access-Control-Allow-Origin"] == "www.raywenderlich.com" else {
response.status(.badRequest).send("Bad origin")
return
}
// 3
response.status(.OK).send("Good origin")
}
Here’s what you just wrote:
- You define the
GET
route you registered on/cors
by specifying arequest
, aresponse
, and anext
handler, which tells your router to continue searching for things to do according to the request that was made. - Next, you validate the value of the
Access-Control-Allow-Origin
header in your response. If you’re wondering why you’d be checking a response without having previously set anything to it, you’re spot on! This is what you will have to fix with middleware. - This is the “happy path.” If everything looks good, simply return a successful response.
Build and run your server, and then confirm that your server is running on port 8080. Open Terminal and execute the following command:
curl -H "Origin: www.raywenderlich.com" localhost:8080/cors
In Terminal, you should see the string "Bad Origin"
sent as a response. This might not be the desired response, but you can trust that it’s expected for now!
You’re going to implement middleware to fix this. In Xcode, open Middleware.swift, and add the following method to the Middleware
class:
// 1
static func initializeCORS(app: App) {
// 2
let options = Options(allowedOrigin: .origin("www.raywenderlich.com"),
methods: ["GET"],
maxAge: 5)
// 3
let cors = CORS(options: options)
// 4
app.router.all("/cors", middleware: cors)
}
Here’s what you just added, step by step:
- The method signature you write to add this middleware as a convenience to your HTTP route.
- You create and set an object of options to enable CORS, most notably that you will only allow
GET
to be an acceptable method on the/cors
route, and that you will only allow requests that specify an origin ofwww.raywenderlich.com
to pass through. ThemaxAge
parameter is a value that specifies how long you want this value to be cached for future requests. - Here, you are creating a
CORS
middleware with your options for use on your HTTP route. - Finally, you register your CORS middleware for all HTTP methods that hit
/cors
on your router. Even though you listedGET
as the only method in youroptions
map, any method should still be able to access this middleware.
Hold down the Command button and click on the CORS
text in your constructor. This will open the CORS
class in Xcode. Scroll to the definition of the handle
method, and add on its first line. Finally, go back to RazeRoutes.swift and at the top of the initializeRazeRoutes
function, add the following to register your middleware:
Middleware.initializeCORS(app: app)
Build and run your server again. Once your server is running on port 8080, execute the same cURL
command in Terminal:
curl -H "Origin: www.raywenderlich.com" localhost:8080/cors
Go back to Xcode, where you’ve hit the breakpoint you’ve just added. Inspect the request
and response
objects as you step through the code. When you finally let the program continue, you should see Good origin
in your response!
Good work! The CORS
middleware took care of ensuring that your response was marked appropriately and allowed a cross-origin resource to access the GET
method on /cors
, all thanks to your middleware! Now let’s do something from scratch.
Middleware From Scratch
Open RazeRoutes.swift again, and at the end of your initializeRazeRoutes
function, register a new GET
route like so:
app.router.get("/raze", handler: razeHandler)
Below your corsHandler
function, add the following code to handle any GET
requests that come in for /raze
:
func razeHandler(request: RouterRequest, response: RouterResponse, next: () -> Void) {
guard let meaning = request.queryParameters["meaningOfLife"] else {
return
}
response.status(.OK).send("Yes, the meaning of life is indeed \(meaning)!")
}
Here’s the drill: You’ve been asked to make a route that simply echoes back the “meaning of life” to any client that makes a GET
request to /raze
, and you want to have control over what that value is. Build and run your server, and execute the following command in Terminal:
curl "localhost:8080/raze?meaningOfLife=42"
You should get a response that says, “Yes, the meaning of life is indeed 42!”.
While truer words may have never been spoken, this route is built on the assumption that all clients know to include this parameter both as a query parameter in the GET
request and to ensure that it is an integer and not a string. You might have good direction, but not every client that consumes this API might remember to include this!
To see what happens if you forget this parameter, execute the following command in Terminal:
curl -v localhost:8080/raze
You should get a response returning a 503 code, meaning that the server is unable to handle the request. What gives? You registered the route and everything, right?
Since you didn’t include the query parameter meaningOfLife
in your request, and you didn’t write code to send back a user-friendly response, it makes sense that you’re going to get a less-than-ideal response in this case. Guess what? You can write middleware to make sure that this parameter is handled correctly in all of your requests to this route!
Further, you can make sure that malformed requests are responded to correctly, so that you can ensure a good developer experience for consumers of this API and not have to worry about touching the original HTTP route code!
In Xcode, open Middleware.swift. Scroll to the very bottom of this file, and add the following code:
// 1
public class RazeMiddleware: RouterMiddleware {
private var meaning: Int
// 2
public init(meaning: Int) {
self.meaning = meaning
}
// 3
public func handle(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) throws {
}
}
What you’ve added, here:
- Kitura requires that your middleware class or struct conforms to the
RouterMiddleware
protocol. - It’s generally a good idea to set up a constructor for your middleware instance. This allows you to handle stored properties that are relevant to your middleware — similar to the options you handled with
CORS
in the previous example. - The single requirement of the
RouterMiddleware
protocol is thehandle
method. It should look familiar, as it takes aRouterRequest
, aRouterResponse
and a closure to tell the router to continue on.
TypeSafeMiddleware
that allows you to use a strongly typed object instead of “raw middleware” as you’ve done here.When you implemented CORS
, you elected to add some headers to your response if and only if your request included certain headers. Inside the handle()
method in your new middleware, add the following code:
guard let parsedMeaning = request.queryParameters["meaningOfLife"] else {
response.status(.badRequest).send("You must include the meaning of life in your request!")
return
}
guard let castMeaning = Int(parsedMeaning) else {
response.status(.badRequest).send("You sent an invalid meaning of life.")
return
}
guard castMeaning == meaning else {
response.status(.badRequest).send("Your meaning of life is incorrect")
return
}
next()
After you register this middleware with the appropriate route, you’ll ensure that you can do the following with incoming requests. All you do in the above code is make sure a meaningOfLife
parameter exists in your requests, make sure it’s a valid number, and finally make sure it’s equal to the correct meaning. If any of these is wrong, you simply respond with an erroneous response. Otherwise, you call next()
to signal this middleware is done with its work.
This might be a fairly contrived example, but consider for a moment that you were able to intervene on all requests made to the /raze
route this way without touching a single line of code on the existing route! This perfectly illustrates the power of middleware. Scroll up to the Middleware
class and the following method to it:
static func initializeRazeMiddleware(app: App) {
let meaning = RazeMiddleware(meaning: 42)
app.router.get("/raze", middleware: meaning)
}
By parameterizing the meaning
value, you can let developers who want to use this middleware set whatever value they want! However, you’re a well-read developer and you understand the true meaning of life, so you set it to 42
here.
Lastly, open up RazeRoutes.swift in Xcode, and inside the initializeRazeRoutes()
function, but above your route registrations, add this line of code:
Middleware.initializeRazeMiddleware(app: app)
Build and run your server, and ensure that it is live on port 8080. Open Terminal, and run the following commands:
curl "localhost:8080/raze"
curl "localhost:8080/raze?meaningOfLife=43"
curl "localhost:8080/raze?meaningOfLife=42"
You should see the following output:
$ curl "localhost:8080/raze"
You must include the meaning of life in your request!
$ curl "localhost:8080/raze?meaningOfLife=43"
Your meaning of life is incorrect
$ curl "localhost:8080/raze?meaningOfLife=42"
Yes, the meaning of life is indeed 42!
- Open Terminal.
- Run the command
lsof -i tcp:8080
. - Note the value under
PID
in the returned text. - Run the command
kill -9 **PID**
with the value of the above PID instead.
- Open Terminal.
- Run the command
lsof -i tcp:8080
. - Note the value under
PID
in the returned text. - Run the command
kill -9 **PID**
with the value of the above PID instead.
Feel free to put breakpoints on your middleware class to observe how it handles each request, but you’ll notice that only the properly formed request gets through to your route handler now. Here’s a reminder: You ensured that this route yields a safe experience no matter what the request is, and you did it all without touching the existing route handler code! Nice work!
Your last example is going to deal with authentication — this might seem scary at first, but the principles are the exact same!