@quell/server is an easy-to-implement Node.js/Express middleware that satisfies and caches GraphQL queries and mutations. Quell's schema-governed, type-level normalization algorithm caches GraphQL query and mutation responses as flattened key-value representations of the graph's nodes, making it possible to partially satisfy queries from the server's Redis cache, reformulate the query, and then fetch additional data from other APIs or databases.
- Installation
- Quick Setup with CLI
- Implementation Guide
- Rate and Cost Limiting Implementation
- Schema Compatibility Layer
- Usage Notes
- Contributors
- Related Documentation
@quell/server is an open-source NPM package accelerated by OS Labs and developed by Cassidy Komp, Andrew Dai, Stacey Lee, Ian Weinholtz, Angelo Chengcuenca, Emily Hoang, Keely Timms, Yusuf Bhaiyat, Chang Cai, Robert Howton, Joshua Jordan, Jinhee Choi, Nayan Parmar, Tashrif Sanil, Tim Frenzel, Robleh Farah, Angela Franco, Ken Litton, Thomas Reeder, Andrei Cabrera, Dasha Kondratenko, Derek Sirola, Xiao Yu Omeara, Nick Kruckenberg, Mike Lauri, Rob Nobile, and Justin Jaeger, Alicia Brooks, Aditi Srivastava, Jeremy Dalton.
If not already installed on your server, install Redis.
- Mac-Homebrew:
- At the terminal, type
brew install redis - After installation completes, type
redis-server - Your server should now have a Redis database connection open (note the port on which it is listening)
- At the terminal, type
- Linux or non-Homebrew:
- Download appropriate version of Redis from redis.io/download
- Follow installation instructions
- Once Redis is successfully installed, follow instructions to open a Redis database connection (note the port on which it is listening)
Install the NPM package from your terminal: npm i @quell/server.
@quell/server will be added as a dependency to your package.json file.
The fastest way to get started with Quell is using our CLI tool, which automatically sets up your project with all necessary files and dependencies.
Run the Quell CLI in your project directory:
npx quell init# Basic initialization
npx quell init
# Initialize with example files
npx quell init --example
# Overwrite existing files
npx quell init --force
# Skip automatic dependency installation
npx quell init --skip-install
# Use JavaScript templates instead of TypeScript
npx quell init --javascriptThe CLI automatically generates these files in your project:
.env- Environment variables for Redis and caching configurationquell-config.ts- Main Quell configuration with schema integration.gitignore- Updated with Quell-specific entries
src/server/example-server.ts- Complete Express server with Quell middlewaresrc/server/schema/example-schema.ts- Sample GraphQL schema with resolvers
package.json- Updated with necessary scripts and dependencies
The CLI automatically installs these packages:
Production Dependencies:
express- Web frameworkgraphql- GraphQL implementationredis- Redis clientdotenv- Environment variable loader
Development Dependencies:
nodemon- Development server with auto-reloadtypescript- TypeScript compilerts-node- TypeScript execution engine@types/express- Express type definitions@types/node- Node.js type definitions
After running the CLI, follow these steps:
Update your .env file with your Redis connection details:
# Local Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# Or Redis Cloud/hosted service
REDIS_HOST=your-redis-host.com
REDIS_PORT=12345
REDIS_PASSWORD=your-passwordReplace the example schema in quell-config.ts with your actual GraphQL schema:
import { QuellCache } from '@quell/server';
import { yourSchema } from './path/to/your/schema'; // Replace this
export const quellCache = new QuellCache({
schema: yourSchema, // Use your actual schema
cacheExpiration: Number(process.env.CACHE_EXPIRATION || 1209600),
redisPort: Number(process.env.REDIS_PORT || 6379),
redisHost: process.env.REDIS_HOST || "127.0.0.1",
redisPassword: process.env.REDIS_PASSWORD || "",
});# Start the development server
npm run dev
# Visit your GraphQL endpoint
# http://localhost:4000/graphqlQuell provides two complementary caching strategies:
- Use Case: Your own GraphQL server with direct schema access
- Caching: Normalized, entity-based caching
- Features:
- Field-level cache invalidation
- Automatic cache key generation
- Mutation-aware cache updates
- Schema introspection for universal compatibility
import { QuellCache } from '@quell/server';
const quellCache = new QuellCache({
schema: yourGraphQLSchema,
cacheExpiration: 3600,
redisHost: process.env.REDIS_HOST,
redisPort: Number(process.env.REDIS_PORT)
});
app.use('/graphql',
quellCache.rateLimiter,
quellCache.costLimit,
quellCache.depthLimit,
quellCache.query,
(req, res) => res.json({ queryResponse: res.locals })
);- Use Case: External GraphQL APIs (GitHub, Contentful, etc.)
- Caching: Query-based key-value caching
- Features:
- Hash-based cache keys
- API-specific cache namespacing
- Custom headers per API
- Automatic cache TTL management
import { createQuellRouter } from '@quell/server';
const quellRouter = createQuellRouter({
endpoints: {
'/graphql': 'local', // Use QuellCache
'/graphql/github': 'https://api.github.com/graphql', // External API
'/graphql/spacex': 'https://api.spacex.land/graphql' // External API
},
cache: quellCache.redisCache,
cacheExpiration: 3600,
debug: true,
headers: {
github: {
'Authorization': 'Bearer your-github-token',
'User-Agent': 'YourApp/1.0'
}
}
});
app.use(quellRouter);Combine both for maximum flexibility:
import express from 'express';
import { QuellCache, createQuellRouter } from '@quell/server';
import { localSchema } from './schema';
const app = express();
// Initialize QuellCache for local schema
const quellCache = new QuellCache({
schema: localSchema,
cacheExpiration: 3600
});
// Create router for handling both local and external APIs
const quellRouter = createQuellRouter({
endpoints: {
'/graphql': 'local', // Routes to QuellCache
'/graphql/external': 'https://api.external.com/graphql'
},
cache: quellCache.redisCache,
debug: process.env.NODE_ENV === 'development'
});
// Apply router first (handles routing logic)
app.use(quellRouter);
// Local GraphQL endpoint (processed after router)
app.use('/graphql',
quellCache.query,
(req, res) => res.json({ queryResponse: res.locals })
);
// Cache management endpoints
app.get('/clear-cache', quellCache.clearCache);
app.get('/clear-external-cache', async (req, res) => {
const cleared = await quellRouter.clearApiCache('external');
res.json({ message: `Cleared ${cleared} external cache entries` });
});That's it! You now have a normalized cache for your GraphQL endpoint.
@quell/server now offers optional cost- and rate-limiting of incoming GraphQL queries for additional endpoint security from malicious nested or costly queries.
Both of these middleware packages use an optional "Cost Object" parameter in the QuellCache constructor. Below is an example of the default Cost Object.
const defaultCostParams = {
maxCost: 5000, // maximum cost allowed before a request is rejected
mutationCost: 5, // cost of a mutation
objectCost: 2, // cost of retrieving an object
scalarCost: 1, // cost of retrieving a scalar
depthCostFactor: 1.5, // multiplicative cost of each depth level
depthMax: 10, // maximum depth allowed before a request is rejected
ipRate: 3 // maximum subsequent calls per second before a request is rejected
}When parsing an incoming query, @quell/server will build a cost associated with the query relative to how laborious it is to retrieve by using the costs provided in the Cost Object. The costs listed above are the default costs given upon QuellCache instantiation, but these costs can be manually reassigned upon cache creation.
If the cost of a query ever exceeds the maxCost defined in our Cost Object, the query will be rejected and return Status 400 before the request is sent to the database. Additionally, if the depth of a query ever exceeds the depthMax defined in our Cost Object, the query will be similarly rejected.
The ipRate variable limits the ammount of requests a user can submit per second. Any requests above this threshold will be invalidated.
Using the implementation described in our "Cache Implementation" section, we could implement depth- and cost-limiting like so:
// instantiate quell-server
const quellCache = new QuellCache({
schema: myGraphQLSchema,
cacheExpiration: 3600,
redisPort: REDIS_PORT,
redisHost: REDIS_HOST,
redisPassword: PASSWORD,
costParameters: { maxCost: 100, depthMax: 5, ipRate: 5 }
});
// GraphQL route and Quell middleware
app.use('/graphql',
quellCache.rateLimit, // optional middleware to include ip rate limiting
quellCache.costLimit, // optional middleware to include cost limiting
quellCache.depthLimit,// optional middleware to include depth limiting
quellCache.query,
(req, res) => {
return res
.status(200)
.send(res.locals);
}
);Note: Both of these middleware packages work individually or combined, with or without the caching provided by quellCache.query.
Quell uses GraphQL introspection to achieve universal schema compatibility:
// Any schema format works
const apolloSchema = makeExecutableSchema({ typeDefs, resolvers });
const vanillaSchema = buildSchema(`type Query { hello: String }`);
const customSchema = new GraphQLSchema({ query: queryType });
// All automatically converted to standardized format
const quellCache = new QuellCache({ schema: anySchema });-
@quell/server reads GraphQL queries from
request.body.queryand attaches responses toresponse.locals.queryResponse -
Cacheable queries must include ID fields (
id,_id,Id, orID) for unique entity identification
- Queries with variables, arguments, nested fields, fragments, and aliases
- Mutations (add/update/delete) with automatic cache invalidation
-
Queries with GraphQL directives (
@include,@skip) -
Subscription operations
-
Queries with introspection fields (starting with
__) -
Queries missing ID fields
-
Universal schema compatibility works with any GraphQL schema format through introspection
@quell/server is an open-source NPM package accelerated by OS Labs and developed by Cassidy Komp, Andrew Dai, Stacey Lee, Ian Weinholtz, Angelo Chengcuenca, Emily Hoang, Keely Timms, Yusuf Bhaiyat, Chang Cai, Robert Howton, Joshua Jordan, Jinhee Choi, Nayan Parmar, Tashrif Sanil, Tim Frenzel, Robleh Farah, Angela Franco, Ken Litton, Thomas Reeder, Andrei Cabrera, Dasha Kondratenko, Derek Sirola, Xiao Yu Omeara, Nick Kruckenberg, Mike Lauri, Rob Nobile, and Justin Jaeger, Alicia Brooks, Aditi Srivastava, Jeremy Dalton.