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.
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
Redis and Vapor With Server-Side Swift: Getting Started
20 mins
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 set
s 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:
- Issue
get
and attempt to map the return value toDog
. Pass that to your closure ascachedDog
. - If
cachedDog
contains a value, unwrap it. - 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.
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 :]