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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. You define the GET route you registered on /cors by specifying a request, a response, and a next handler, which tells your router to continue searching for things to do according to the request that was made.
  2. 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.
  3. 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:

  1. The method signature you write to add this middleware as a convenience to your HTTP route.
  2. 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 of www.raywenderlich.com to pass through. The maxAge parameter is a value that specifies how long you want this value to be cached for future requests.
  3. Here, you are creating a CORS middleware with your options for use on your HTTP route.
  4. Finally, you register your CORS middleware for all HTTP methods that hit /cors on your router. Even though you listed GET as the only method in your options 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:

  1. Kitura requires that your middleware class or struct conforms to the RouterMiddleware protocol.
  2. 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.
  3. The single requirement of the RouterMiddleware protocol is the handle method. It should look familiar, as it takes a RouterRequest, a RouterResponse and a closure to tell the router to continue on.
Note: In the last route you implement, you’ll use a different version of this type of middleware called 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!
  1. Open Terminal.
  2. Run the command lsof -i tcp:8080.
  3. Note the value under PID in the returned text.
  4. Run the command kill -9 **PID** with the value of the above PID instead.
Note: If your console ever tells you that another server is already listening on port 8080, follow these steps to clean up your port:

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!

Contributors

Cesare Rocchi

Tech Editor

Adriana Kutenko

Illustrator

Shai Mishali

Final Pass Editor

Tim Condon

Team Lead

Over 300 content creators. Join our team.