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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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 :]