Redis and Vapor With Server-Side Swift: Getting Started

Learn how to use the in-memory data store, Redis, and Vapor to cache objects by saving them in JSON, then configuring them to expire after a set time. By Walter Tyree.

Leave a rating/review
Download materials
Save for later
Share

Redis is the most popular key-value data store for high-performance storage and caching. In this tutorial, you’ll learn how to add Redis to a Vapor project to cache API responses. You’ll also explore some of the other features of Redis using the command-line interface (CLI).

To do this, you’ll add caching to a new web app: Your Daily Dog. This app fetches a new image of a cute dog to display on your site. At the beginning of the tutorial, the app retrieves a new dog record from the API for each page reload. By the end of the tutorial, you’ll ensure that everyone who visits during a set period of time will see the same dog picture.

When speed is critical, a Redis cache can help your app be “Best in Show”. Other tricks Redis can do include:

  • Storing session states
  • Replacing parts of a database
  • Maintaining a sorted list
  • Storing and brokering messages

This app uses data from The Dog API, which is a public API for viewing cute dog pictures. Hopefully you aren’t allergic to dogs. :]

Note: This tutorial assumes you have experience using Vapor to build web apps and are comfortable with the command line and Docker. If you are 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.

This tutorial uses Leaf to render web pages. For more information about Leaf, read Templating Vapor Applications With Leaf.

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 has been tested on macOS and Ubuntu.

Open the Vapor app in Xcode by double-clicking Package.swift. If you’re not using Xcode, open Terminal and navigate into the starter folder. Then type swift build to pull all the dependencies and build the app. The project contains three folders:

  • Resources: Contains the web template used to render your app’s web page.
  • Tests: Contains a test for one of your routes.
  • Sources: Contains the source files for your Vapor app.

Open Sources/App/routes.swift. You’ll see two routes. app.get("hello") is a placeholder route to make sure your app is running and listening. A test in the Testing folder will exercise this route. app.get is the more exciting route. Here’s how it looks:

app.get { req -> EventLoopFuture<View> in
  //1
  let apiURL = URI("https://api.thedogapi.com/v1/images/search")
  //2
  return req.client.get(apiURL).flatMap { res -> EventLoopFuture<View> in
  //3
  guard let freshDog =
    try? res.content.decode(Array<Dog>.self).first else {
      return req.view.render("index")
    }
    //4
    return req.view.render("index", freshDog)
  }
}

This route does a few things. It:

  1. Defines apiURL to point to the endpoint, where it will fetch a dog record.
  2. Issues a get request to the URL.
  3. Attempts to decode the response from the DogAPI as Dog.
  4. Passes the decoded Dog to Leaf for rendering.

Next, go to Dog.swift, where the coding magic happens. By conforming to the Content protocol, Vapor automatically manages all the encoding and decoding for you. This lets you easily use the structs in your app.

Running the App

Enter swift run in the command line. On Linux, you may need to type swift run --enable-test-discovery, depending on what flavor and version combination you have.

For Xcode, you need to set the working directory so Vapor can find the Leaf template. To do that, open the scheme editor by left-clicking dailydog in the top bar to open the pop-up, then selecting Edit Scheme.

Scheme editor pop-up with Edit Scheme selected

Under Options, select Working Directory, then click the folder icon and select the root of your project.

Working directory option with the project root selected

Once you’ve selected your project, click Choose.

Working Directory window with the Choose button in the bottom-right

Now, build and run.

Xcode window with the starter project

Regardless of how you got your project running, use a web browser to navigate to localhost:8080 to enjoy a picture of a cute dog, like this one:

A cute dog on the website

Note: The API you’re using in this tutorial is maintained by regular people who take pictures of their dogs. Some of the records are not complete. You may see an error message on your web page like this: {"error":true,"reason":"expected array at key: breeds"}. If that happens, keep reloading the page until a dog appears.

Every time you reload the page, you’ll see another dog. Your next task will be to add Redis and use it to cache a dog so everyone can enjoy the same picture for a while.

Adding Redis

Vapor has built-in support for Redis, which uses RediStack behind the scenes. See the official documentation for more details.

Your first step is to open Package.swift. Then, add a dependency for the Vapor/Redis library in the dependencies section:

.package(url: "https://github.com/vapor/redis.git", from: "4.0.0"),

Now, link the package to an import product in the app’s dependencies section:

.product(name: "Redis", package: "redis"),

Now that you’ve imported the Redis library, you need to tell it where to find the Redis server. If you’re using Docker, the Redis server will be at localhost. When you use Redis in production, it will be at a different IP address.

Note: Redis is built for speed. When you plan your Redis setup, you want the Redis server as close to your Vapor app as possible. If Redis is at some remote data center and communicating over TCP/IP, you’ll lose its benefit.
To achieve maximum speed, Redis assumes that it’s in a trusted environment. That means it doesn’t encrypt things, as a slower database server might. You can learn more about securing Redis at the official documentation.

Open configure.swift and add Redis to the import statements at the top:

import Redis

Finally, initialize Redis on startup by replacing TODO: Configure Redis line with:

app.redis.configuration = try RedisConfiguration(hostname: "localhost")

Starting Docker

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 Redis container by entering the following into the command line:

docker run --name dog-cache -p 6379:6379 -d redis

This creates a container named dog-cache running on the standard Redis port of 6379. The -d flag detaches the process from the current shell.

Return to routes.swift and add Redis to the import statements at the top:

import Redis

Next, find the app.get("hello") route and replace this line:

return req.eventLoop.makeSucceededFuture("Hello, world!")

with the following:

return req.redis.ping()

Build and run. This time, browse to localhost:8080/hello.

PONG output from Redis displayed in the web page

You’ll see the word PONG in an otherwise blank webpage. The /hello route executed the Redis .ping() command. The Redis server responded with PONG. This is the standard connectivity test, which you can learn more about in the Redis documentation.

So far, you’ve gotten Redis running and connected it to your Vapor app. However, when you refresh the web page at localhost:8080, the dogs keep changing. For your next step, you’ll update the app so that when you fetch the first dog, Redis will cache it and return the same dog on future requests.

Caching a JSON Object

Redis uses a key-value pattern to store objects, so your first step is to create a key.

Open routes.swift and replace TODO: Add Redis Key with this code to create a key:

let todaysDog = RedisKey("cuteDog")

RedisKey is a thin wrapper around String that helps ensure your key is valid.

Now, to add the key to Redis to store the dog image, replace TODO: Cache freshDog with:

req.redis.set(todaysDog, toJSON: freshDog).whenComplete { result in
  switch result {
  case .success:
    print("Dog was cached.")
  case .failure(let error):
    print("Dog was not cached. Error was: \(error)")
  }
}

This uses the Redis set command to store Dog with the key. set always takes a string, even if it’s a numerical value. There are different sets for more complex data.

toJSON: is a convenience in Vapor Redis library, not part of Redis itself. Your object conforms to the Vapor Content protocol, which lets the library encode and decode the object to a JSON string so you can store it in Redis.

Any time you interact with Redis, even though it’s fast, the response returns as an EventLoopFuture. In this example, you use .whenComplete to handle the Result of the type <Void, Error>.

Build and run. Now, when you reload the page, the dog still changes, although it’s cached in Redis. When set succeeds, a message is printed in the console.

[ INFO ] GET / [request-id: DE67A24D-9466-4273-978E-482C73C6F076]
Dog was cached.
[ INFO ] GET / [request-id: F62D201B-5E39-42F0-81A8-0F630231E1C8]
Dog was cached.

Next, you’ll see why the cached dog isn’t displaying.

Inspecting the Cache

So why are you still seeing new dogs when you reload the page? Well, you are just caching data, but not displaying cached data in the web page. Let’s inspect the cache first, using the CLI. Open a new Terminal window and type:

docker exec -it dog-cache redis-cli

If you’re using Docker Desktop, clicking the CLI button for the container will open the CLI for that container. You then need to issue the redis-cli command to enter the CLI for Redis. You’ll know you’re in the right place when your Terminal prompt turns into an IP number and the Redis port number of 6379.

~> docker exec -it dog-cache redis-cli
127.0.0.1:6379> 

Now, examine the cache to confirm that the Vapor app is caching the dogs. Since Vapor stores the dog using the key cuteDog, you get the data from the cache using the same key. Try it by entering the following into the Redis CLI:

GET cuteDog

You’ll see an output similar to this:

127.0.0.1:6379> GET cuteDog
"{\"breeds\":[{\"weight\":{\"metric\":\"6 - 7\",\"imperial\":\"14 - 16\"},\"reference_image_id\":\"r1Ylge5Vm\",\"height\":{\"metric\":\"25 - 28\",\"imperial\":\"10 - 11\"},\"id\":24,\"bred_for\":\"Cattle herdering, hunting snakes and rodents\",\"life_span\":\"15 years\",\"breed_group\":\"Terrier\",\"name\":\"Australian Terrier\",\"temperament\":\"Spirited, Alert, Loyal, Companionable, Even Tempered, Courageous\"}],\"id\":\"r1Ylge5Vm\",\"width\":1081,\"url\":\"https:\\/\\/cdn2.thedogapi.com\\/images\\/r1Ylge5Vm_1280.jpg\",\"height\":720}"
127.0.0.1:6379>

From the JSON that appears, you can verify that it is, indeed, the same dog your web app is showing. Reload the web page a few times and execute the CLI get command a few times until you’re satisfied that writing to the cache is working.

You can exit the CLI at any time by typing:

QUIT

Leave it open for now, however. You’ll need it later in this tutorial.

Reading from the Cache

Navigate to routes.swift again. Next, you’ll read from the cache by adding this code, just after the definition of todaysDog:

//1
return req.redis.get(todaysDog, asJSON: Dog.self).flatMap { cachedDog in
    //2
    if let cachedDog = cachedDog {
      //3
      return req.view.render("index", cachedDog)
    }

Here’s what’s happening above:

  1. Issue get and attempt to map the return value to Dog. Pass that to your closure as cachedDog.
  2. If cachedDog contains a value, unwrap it.
  3. Render the web page with the cached dog.

Now, substitute the comment // TODO: Add curly bracket with a closing curly bracket.
To fix the indentations, select the whole route and press Control-I. The final code looks like the following:

app.get { req -> EventLoopFuture<View> in
  let apiURL = URI("https://api.thedogapi.com/v1/images/search")
  let todaysDog = RedisKey("cuteDog")
  //1
  return req.redis.get(todaysDog, asJSON: Dog.self).flatMap { cachedDog in
    //2
    if let cachedDog = cachedDog {
      //3
      return req.view.render("index", cachedDog)
    }
    return req.client.get(apiURL).flatMap { res -> EventLoopFuture<View> in
      guard let freshDog = try? res.content.decode(Array<Dog>.self).first else {
        return req.view.render("index")
      }
      req.redis.set(todaysDog, toJSON: freshDog).whenComplete { result in
        switch result {
        case .success:
          print("Dog was cached.")
          expireTheKey(todaysDog, redis: req.redis)
        case .failure(let error):
          print("Dog was not cached. Error was: \(error)")
        }
      }
      return req.view.render("index", freshDog)
    }
  }
}

Build and run. Now, no matter how often you reload, the dog that you see will be the same! Just like set, get also returns a future. That’s why you use .flatMap to keep everything straight.

Expiring a cache entry

You’ve gone from getting a new dog with each refresh to caching one dog for eternity. Neither option is exactly what you want. For your next step, you’ll set an expiration time on the key, so that the dogs will change after a set amount of time. In other words, you’ll clear the cache after the time you set passes.

Edit routes.swift and add this function anywhere outside of routes:

private func expireTheKey(_ key: RedisKey, redis: Vapor.Request.Redis) {
  //This expires the key after 30s for demonstration purposes
  let expireDuration = TimeAmount.seconds(30)
  _ = redis.expire(key, after: expireDuration)
}

The Vapor Redis library uses TimeAmount from the Swift NIO library. This lets you set time in whatever magnitude makes the most sense for your app. TimeAmount handles values from nanoseconds to hours, but Vapor Redis wants all expirations in seconds.

The function above takes the key and the current Redis client and executes .expire for that key. In a real app, you’d probably want to change the dog image on a daily or hourly basis. For this example, however, you’ll hard code an expiration of 30 seconds so you can see it in action.

Overwriting the key with set removes the expiration, so you’ll need to reset it. To apply the expiration after every set, call the function right after print("Dog was cached."), like so:

expireTheKey(todaysDog, redis: req.redis)

Remember that any interaction with Redis will return a future, so you need to chain them together to guarantee the right order of execution.

The only problem left is that the dog that’s cached in Redis now will never expire. This means the code to fetch a new dog will never execute.

Go to the terminal window that’s running the Redis CLI and delete the existing entry by typing in:

DEL cuteDog

Build and run. Now, the web app will return the same dog image for all subsequent requests that happen within the 30 seconds following the first request. After that, it retrieves a new one.

Final result with cache implemented

After reloading the web page a few times, go back to the Redis CLI window and enter:

TTL cuteDog

This command returns a value equal to the number of seconds until the key expires. Run it a few times to verify that the value changes as time goes by.

127.0.0.1:6379> TTL cuteDog
(integer) 22
127.0.0.1:6379> TTL cuteDog
(integer) 21
127.0.0.1:6379> TTL cuteDog
(integer) 20

If the key already expired, it returns -1. If no key exists, it returns -2.

Find the full documentation for TTL in the Redis documentation.

Congrats, now Your Daily Dog can cache images and you can configure for how long images can be cached. Your customers are already appreciating the speed of the new web page :]

Where to Go From Here?

In this tutorial, you learned how to run Redis in Docker with your Vapor app. You also saw how to cache and retrieve an encodable object and how to expire a key.

This was only a small part of what Redis can do. Here are some suggestions for other ways to use Redis:

  • Track a user’s session or page visits by opening the Redis CLI and entering a new key with a value of 0:
    SET pageCount 0
    
  • To track how many people have visited a page, increase pageCount using the Redis INCR command, like so:
    INCR pageCount
    
  • To retrieve the current value type:
    GET pageCount
    

    You’ll notice that the value returned is greater. In a Vapor app, the command looks a little different: req.redis.increment(RedisKey("pageCount")).

Using these commands lets you easily add buttons for upvotes and downvotes to the dog pages, for example.

You can find the complete documentation for INCR and its counterpart, DECR, at the Redis documentation site.

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

The Vapor Redis library is built on RediStack. A full list of the commands available to your Vapor app can be found at the RediStack documentation site.

It’s also helpful to look at the redis and RediStack header files. Sometimes there’s extra documentation or a slight wording change between the actual Redis command and how it’s implemented for Vapor. INCR is one example of that.

If you have any questions or comments, please join the forum discussion below.