Home Server-Side Swift Tutorials

Middleware Tutorial for Server-Side Swift Using Vapor 4: Getting Started

In this tutorial, you’ll learn how to create middleware — a module that sits between an app and the browser and removes some of the work load from your web app.

5/5 2 Ratings

Version

  • Swift 5, macOS 10.15, Xcode 12

Middleware facilitates the single-responsibility principle by allowing you to create small modules with a sole purpose. In turn, this speeds up development, makes your code more modular and simplifies unit testing. In the context of a web server, middleware sits between the web app and the browser and performs tasks like logging and third-party authentication. This allows your web app to focus on its primary functions.

In this tutorial, you’ll enhance a Vapor project by adding middleware to a web app. By the end, you’ll

  • Understand the steps of creating middleware.
  • Know how to add an authorization mechanism to your backend.
  • Learn how to add formatted entries to server logs.
  • Be able to limit who can change the database of URLs.
  • End up with better and more useful logging.

You’ll do this all with an app, lnkshrtnr, which will be the next great startup success. Everyone can tell because it has a clever name! :]

lnkshrtnr allows anyone to create a shortened version of a long URL. When someone uses the shortened URL, lnkshrtnr will expand the link and redirect the browser to the long URL. Shortened links are popular in blog posts and conference talks because they take up less space.

Currently, though, lnkshrtnr has two major flaws. The first is anyone can add links, so malicious users can add dangerous links to the app. The second is the generic logging makes it hard to understand how the app is being used.

Note: This tutorial assumes you have experience using Vapor to build web apps and are comfortable with the command line and Docker. If you’re new to Vapor, check out the excellent Getting Started with Server-Side Swift With Vapor 4 tutorial. For a comprehensive tutorial on Docker, visit Docker on macOS: Getting Started.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial. Then, navigate to the starter folder. The sample app works on macOS or Ubuntu.

This project consists of a database hosted in a Docker container for storing the link shortening information. It has a public GET route for redirecting links, along with administrative routes for adding and deleting database records. There are also tests.

Open the Vapor app in Xcode by double-clicking Package.swift. If you’re not using Xcode, open Terminal or your command line application if using Linux and navigate to the starter folder. Next, type swift build to begin the process of pulling down the dependencies and building the app. While your computer is downloading and building dependencies, explore the project.

At the root level, you’ll find LnkShrtn.postman_collection.json, which is a file containing some scripts for exercising the API using Postman.

Navigate to Sources/App and open the routes.swift file. Here, you’ll find the main public GET route of the app and a sample curl command to run if you aren’t using Postman. You’ll also find a line that looks like this:

try app.register(collection: AdminRoutes())

The project organizes all the administrative routes in a separate file. Open Sources/App/AdminRoutes.swift now. As with routes.swift, each route has a comment with a curl command to use as an example. The admin.post("shorten") route lets you pass in a key and a long URL to add to the database. It returns a status of conflict if the key already exists in the database, and a status of created if it successfully adds your data. There are also routes to delete and view records in the database.

Finally, open Sources/App/configure.swift and find the //TODO: Add Middleware line. After you create middleware, adding it to the app configuration applies it to all the traffic.

Once the system finishes downloading and building, it’s time to try everything out!

This tutorial assumes you have the Docker daemon installed and running. See the note at the beginning of this tutorial if you need help. Start a Docker Postgres container using docker-compose.yml by typing the following into the command line for Mac:

docker compose up db

Or, if you’re using Linux, type the following:

docker-compose up db

If you don’t have Docker Compose installed, you can type the regular Docker command of:

docker run --name linkshrt-test \
-e POSTGRES_DB=vapor_test_database \
-e POSTGRES_USER=vapor_test_username \
-e POSTGRES_PASSWORD=vapor_test_password \
-p 5432:5432 -d postgres:latest

No matter how you get the database running, start the app server next. In Xcode, use Product > Run or Command-R. In Terminal, type swift run.

Note: Swift projects don’t have an .xcproject file, so Xcode may complain about missing resources and refuse to launch the project. Alternately, the project may launch, but the .env file containing important project-specific values won’t be found in the working directory and will cause the app to crash. The Vapor docs have a comprehensive webpage on how to modify your Scheme to designate the proper working directory.

In AdminRoutes.swift, inside of boot, you’ll find the curl commands required to create a shortened link. For example, look at this line:

-d '{"shortUrl": "test", "longUrl": "http://www.example.com"}'\

You’ll find the value test for the key shortUrl. This is the newly created shortened URL that will point to the value you place under the key longUrl — the value is currently http://www.example.com".

With both the database and the server running, use curl or Postman to add some links to the database. Then browse to localhost/yourShortUrlValue and try them out. Also, the eight tests in the test suite should pass.

Adding Middleware

As stated before, middleware will execute between the browser and the request handler. This means that code for handling a specific route can focus on that task; it doesn’t need to concern itself with authorization, file handling, logging or any other tasks. This is why middleware is a great example of the single-responsibility principle. Middleware instances sit between your router and the client connected to your server. This allows them to view, and potentially mutate, incoming requests before they reach your controllers. A middleware instance may choose to return early by generating its own response, or it can forward the request to the next responder in the chain. The final responder is always your router. When the response from the next responder is generated, the middleware can make any modifications it deems necessary, or choose to forward it back to the client as is. This means each middleware instance has control over both incoming requests and outgoing responses.

Middleware execution order

API Key for Authorization

For your first piece of middleware, you’ll add a way to protect the administrative routes.

Authorization vs. Authentication

A common method of authorization for APIs is an API key. Generally, you request a secret key from an API provider after providing whatever contact or billing information they require. They’ll send you the key in an email, and then every request you make will need to include that API key in the header.

Middleware can determine if the key, and therefore the request, is valid. This way, the application doesn’t need to concern itself with verifying requests. Authentication is how someone verifies that you are who you say you are, and the API vendor went through authentication before sending you your key. Once you have the API key or some other credential, you go through the process of authorization for every request you make. Authorization determines whether the server should fill a request.

Start by finding Sources/App/Middleware/APIKeyCheck.swift and opening it. All middleware needs to implement func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response>. This one does that, but nothing more.

Note that there can be any amount of middleware in a responder chain, each of which passes along to the next after performing whatever tasks. Eventually, your app will be the recipient of the request.

Find the line // TODO: Add API Key Check and replace it with the following:

  // 1
  guard let apiKey: String = request.headers["x-api-key"].first,
    // 2
    let storedKey = Environment.get("API_KEY"),
    apiKey == storedKey else {
      // 3
      return request.eventLoop.future(error: Abort(.unauthorized))
    }

Here’s what this code does:

  1. Searches the request for the header containing the API key.
  2. Loads an API key from the server’s environment.
  3. Immediately returns with an unauthorized error if the stored key and the header don’t match.
Note: Never hardcore API keys and other secrets. Instead, put them somewhere safe and load them at runtime. Vapor provides a special class, Environment, which reads from a hidden .env file. When you deploy your app on a service like Heroku or Azure, they’ll have other ways to provide secrets to your app. The documentation for the Vapor Environment can provide more information and help you create a more robust .env file.

All the code for this middleware is in a guard statement. If a valid API key is present, there’s nothing to do and the request is passed along.

Protecting a Group

During the tour of the project, you found where to add middleware in configue.swift. You could add your new middleware to that part of the app by inserting the following after the line for FileMiddleware:

app.middleware.use(APIKeyCheck())

However, this would check for an API key on every request, and you only want to protect the administrative routes.

So, open Sources/App/AdminRoutes.swift and find // TODO: Protect Routes. Modify the line that creates the group so that it looks like the following:

let protected = routes.grouped(APIKeyCheck())
protected.group("admin") { admin in

This creates a new grouping called protected, and then uses it to group all the routes. Note the use of the operator grouped instead of the operator group. Using grouped assigns a variable you can use for composing more complicated routing, where group has a closure for its routes.

Now the API checking middleware only applies to the routes in this group instead of all of the routes. Build and run.

The standard GET command will still work, but any commands to the administrative routes won’t. If you’re using Postman, enable the API key by turning it on in the header section. If you’re using curl, modify the commands to add a new -H parameter like what’s shown in the example below:

curl -i -X POST -H "Content-Type: application/json" \
-H "x-api-key: 4310f636-43ec-41ba-aa34-b3e3c378d687" \
-d '{"shortUrl": "rw", "longUrl": "http://www.raywenderlich.com"}' \
http://localhost:8080/admin/shorten

Add the API key to commands and confirm everything is working. If you change the API key in the .env file and rebuild the app, you’ll have to change the API key in all your requests.

Bringing Middleware From Vapor to Your Projects

In addition to any middleware you write, Vapor has middleware you can bring into your projects. Unless you explicitly override them, the middleware for logging and error are always included and will be the first middleware in the chain. If you ever want to override them, before the first app.middleware.use() statement in your configure.swift, add app.middleware = init() to clear the defaults.

The files middleware lets you provide a directory that holds static assets, like CSS files, images and JavaScript files. Any web requests for these assets are returned directly without burdening your routes.swift file with the extra work.

By default, the files middleware uses the Public folder in a Vapor project. If you open the configure.swift of this project, you can see the file middleware is enabled. If you comment out the middleware and run the app, you’ll see some errors in the logs when you use the app. This is because when browsers make GET requests, they ask for favicon files, which are the little files that show up in a browser’s tab bar. Without the file middleware enabled, a Vapor app doesn’t have any icons to return.

lnkshrtnr uses API keys for authorization on single requests. When you want to have usernames and passwords and session management, Vapor provides the Sessions middleware to help you manage session state. There’s an excellent tutorial if you want more information on how to implement that.

Vapor also provides CORS middleware. CORS is necessary when you have different parts or resources for your app hosted at different domains. This will most commonly happen when using web fonts or a cloud hosting provider with many different microservices. You’ll know it’s time to implement CORS when you see errors about “Cross-Origin Request Blocked” or “Same Origin Policy” in the logs. An important thing to pay attention to with CORS is where it sits in the list of middleware.

Testing Requests

Now that the API key is protecting them, your tests for the admin routes are all failing. Run the tests now and you’ll see four failed tests. In Xcode, run tests using Command-U or Product > Test. When using Terminal, type swift test.

The reason tests fail is because the API key isn’t in the test headers. Vapor includes some extensions specifically for helping you modify headers when sending test requests, so you can read the documentation to get a complete understanding.

First, open Tests/AppTests/AdminTests.swift and create a helper method at the bottom of the class that will add the header value to the admin requests:

  private func addAuthHeader(request: inout XCTHTTPRequest) {
    request.headers.add(name: "x-api-key", value: apiKey)
  }	

Notice the use of the inout keyword. inout makes the request mutable as a parameter so you don’t have to copy it to a variable in the helper method.

To add your API key to a test request, find testRemoveLink(). Change the test so that it looks like the code below. Notice that you’re adding beforeRequest and afterResponse in the middle of the current code:

  // 1
  try testApp.test(.DELETE, "admin/omw3", beforeRequest: {request in
    // 2
    addAuthHeader(&request)
  },
  // 3
  afterResponse: {res in
    XCTAssert(res.status == HTTPResponseStatus.noContent,
      "Expected a status of NO CONTENT but got \(res.status)")
  //4
  })

Here’s what this code does:

  1. Adds the parameter of beforeRequest to let you modify request before sending it to the server.

  2. Adds the apiKey value to the correct header.

  3. Assigns the trailing closure to the afterResponse parameter.

  4. Adds a close parenthesis now that you no longer have a trailing closure.

Now, run the tests again and this one should pass. See if you can figure out how to make the other three tests pass. If you get stuck, look at the tests in the Final project of the download materials.

Logging Responses

Thus far, the middleware has been acting on a request object. However, it can also interact with a response object as it leaves the server and returns to the browser. This next middleware will only log redirected responses. You’ll tag the log entries to make it easier for someone to extract them later and do further analysis.

Open Sources/App/Middleware/Logging.swift. As with the API file, it only has the one required protocol method:

func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {

It’ll be called as part of the request chain. Notice that the method also has a Responderproperty called next. That property is an EventLoopFuture because the response hasn’t happened yet. When you write code for the future property, it won’t execute until the response object exists. Update the return line and log things in the future:

  // 1
  return next.respond(to: request).map { res in
  // 2
    if res.status == .movedPermanently {
    // 3
      request.logger.info("[REDIRECT] \(request.url.path) -> \(res.headers[.location])")
    }
    return res
  }

Here’s what the logger does now:

  1. Adds .map to the standard return next.respond(to:) to let you access the future response as res.
  2. Uses an if statement so you’ll only log responses that were redirects.
  3. Writes a message to the standard logger object at the .info level.

The last step is to enable your new logging middleware. Open configure.swift and replace TODO:// Add middleware with app.middleware.use(Logging()). Run the app again. Notice that when you request one of the short links you added earlier, there will be a new log statement.

Note: Many browsers cache redirect requests by default. If your browser does this, a log statement won’t be executed because the browser is performing the redirect and bypassing the server. To prevent this, you can use Incognito mode, or clear your browser’s cache after enabling logging.

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned about middleware, including how to add an API key to protect some of the routes of your web app, and how to add specially formatted entries to the server logs.

If you want to do more with your project, consider:

  • Using Redis or some other data store to have many different API keys.
  • Enhancing the logging to make it easy for API keyholders to report on how their short links are being used.

If you’re interested in learning more, the Vapor Docs contain more information and resources on middleware.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments