DenoStore brings modular and low latency caching of GraphQL queries to a Deno/Oak server.
DenoStore Query Demo
- Description
- Features
- Installation
- Getting Started
- Further Documentation
- Contributions
- Developers
- License
When implementing caching of GraphQL queries there are a few main issues to consider:
- Cache becoming stale/cache invalidation
- More unique queries and results compared to REST due to granularity of GraphQL
- Lack of built-in caching support (especially for Deno)
DenoStore was built to address the above challenges and empowers users with a caching tool that is modular, efficient and quick to implement.
- Seamlessly embeds caching functionality at query resolver level, giving implementing user modular decision making power to cache specific queries and not others
- Caches resolver results rather than query results - so subsequent queries with different fields and formats can still receive existing cached values
- Leverages Redis as an in-memory low latency server-side cache
- Integrates with Oak middleware framework to handle GraphQL queries with error handling
- Provides global and resolver level expiration controls
- Makes GraphQL Playground IDE available for constructing and sending queries during development
- Supports all GraphQL query options (e.g. arguments, directives, variables, fragments)
DenoStore uses Redis data store for caching
- If you do not yet have Redis installed, please follow the instructions for your operation system here: https://redis.io/docs/getting-started/installation/
- After installing, start the Redis server by running
redis-server
- You can test that your Redis server is running by connecting with the Redis CLI:
redis-cli
127.0.0.1:6379> ping
PONG
-
To stop your Redis server:
redis-cli shutdown
-
To restart your Redis server:
redis-server restart
-
Redis uses port
6379
by default
DenoStore is hosted as a third-party module at https://deno.land/x/denostore and will be installed the first time you import it and run your server. It is recommended to specify the latest DenoStore version so Deno does not use a previously cached version.
import { DenoStore } from 'https://deno.land/x/denostore@<latestversion>/mod.ts';
DenoStore uses the popular middleware framework Oak https://deno.land/x/oak to set up routes for handling GraphQL queries and optionally using the GraphQL Playground IDE. Like DenoStore, Oak will be installed directly from deno.land the first time you run your server unless you already have it cached.
Using v10.2.0 is highly recommended
import { Application } from 'https://deno.land/x/oak@v10.2.0/mod.ts';
Implementing DenoStore takes only a few steps and since it is modular you can implement caching to your query resolvers incrementally if desired.
To set up your server:
- Import Oak, DenoStore class and your schema
- Create a new instance of DenoStore with your desired configuration
- Add the route to handle GraphQL queries ('/graphql' by default)
Below is a simple example of configuring DenoStore for your server file, but there are several configuration options. Please refer to the docs for more details.
// imports
import { Application } from 'https://deno.land/x/oak@v10.2.0/mod.ts';
import { DenoStore } from 'https://deno.land/x/denostore@<latestversion>/mod.ts';
import { typeDefs, resolvers } from './yourSchema.ts';
const PORT = 3000;
const app = new Application();
// configure DenoStore instance
const ds = new DenoStore({
route: '/graphql',
usePlayground: true,
schema: { typeDefs, resolvers },
redisPort: 6379,
});
// add dedicated route
app.use(ds.routes(), ds.allowedMethods());
How do I set up caching?
After your DenoStore instance is configured in your server, all GraphQL resolvers have access to that DenoStore instance and its methods through the ds
property in each resolver's context
object argument. Your schemas do not require any DenoStore imports.
Accessing DenoStore methods using ds
from context
oneRocket: async (
_parent: any,
args: any,
// destructuring ds off context
{ ds }: any,
info: any
)
Alternatively, you can access ds from context without destructuring (e.g. context.ds.cache
)
Here is an example of a query resolver before and after adding the cache method from DenoStore. This is a simple query to pull information for a particular rocket from the SpaceX API.
No DenoStore
Query: {
oneRocket: async (
_parent: any,
args: any,
context: any,
info: any
) => {
const results = await fetch(
`https://api.spacexdata.com/v3/rockets/${args.id}`
)
.then(res => res.json())
.catch(err => console.log(err))
return results;
},
DenoStore Caching
Query: {
oneRocket: async (
_parent: any,
args: any,
{ ds }: any,
info: any
) => {
return await ds.cache({ info }, async () => {
const results = await fetch(
`https://api.spacexdata.com/v3/rockets/${args.id}`
)
.then(res => res.json())
.catch(err => console.log(err))
return results;
});
},
As you can see, it only takes a few lines of code to add modular caching exactly how and where you need it.
Cache Method
ds.cache({ info }, callback);
cache
is an asynchronous method that takes two arguments:
- An object where info is the only required property. The GraphQL resolver's info argument must be passed as a property in this object as DenoStore parses the info AST for query information
- A callback function with your data store call to execute if the results are not in the cache
Expiration time for cached results can be set for each resolver and/or as a global default.
You can easily pass in cache expiration time in seconds as a value to the ex
property to the cache method's first argument object:
// cached value will expire in 5 seconds
ds.cache({ info, ex: 5 }, callback);
You can also add the defaultEx
property with value expiration time in seconds when configuring the ds
instance on your server.
// configure DenoStore instance
const ds = new DenoStore({
route: '/graphql',
usePlayground: true,
schema: { typeDefs, resolvers },
redisPort: 6379,
// default expiration set globally to 5 seconds
defaultEx: 5,
});
When determining expiration for a cached value, DenoStore will always prioritize expiration time in the following order:
ex
property in resolvercache
methoddefaultEx
property in DenoStore configuration- If no resolver or global expiration is set, cached values will default to no expiration. However, in the next section we discuss ways to clear the cache
There may be times when you want to clear the cache in resolver logic such as when you perform a mutation. In these cases you can invoke the DenoStore clear
method.
Mutation: {
cancelTrip: async (
_parent: any,
args: launchId,
{ ds }: any
) => {
const result = await dataSources.userAPI.cancelTrip({ launchId });
if (!result)
return {
success: false,
message: 'failed to cancel trip',
};
// clear/invalidate cache after successful mutation
await ds.clear();
return result;
},
You can also clear the Redis cache at any time using the redis command line interface.
Clear keys from all databases on Redis instance
redis-cli flushall
Clear keys from all databases without blocking your server
redis-cli flushall async
Clear keys from currently selected database (if using same Redis client for other purposes aside from DenoStore)
redis-cli flushdb
We welcome contributions to DenoStore as they are key to growing the Deno ecosystem and community.
- Fork and clone the repository
- Ensure Deno and Redis are installed on your machine
- Redis server must be running to use DenoStore
- Checkout feature/issue branch off of main branch
- Make sure Redis server is running on port 6379 when testing
- To run all tests run
deno test tests/ --allow-net
- If tests pass you can submit a PR to the DenoStore main branch
This product is licensed under the MIT License - see the LICENSE.md file for details.
This is an open source product.
This product is accelerated by OS Labs.