10.0.0 • Published 5 months ago

@quell/server v10.0.0

Weekly downloads
3
License
MIT
Repository
github
Last release
5 months ago

License: MIT AppVeyor AppVeyor contributions welcome

@quell/server

@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.

JavaScript NodeJS Express.js TypeScript Jest Testing-Library Redis GraphQL React Postgres MySQL MongoDB TailwindCSS

Table of Contents


@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.

Installation

Installing and Connecting a Redis Server

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)
  • 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 @quell/server

Install the NPM package from your terminal: npm i @quell/server. @quell/server will be added as a dependency to your package.json file.


Quick Setup with CLI

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.

Installation

Run the Quell CLI in your project directory:

npx quell init

CLI Options

Basic Commands

# 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 --javascript

What the CLI Creates

The CLI automatically generates these files in your project:

Configuration Files

  • .env - Environment variables for Redis and caching configuration
  • quell-config.ts - Main Quell configuration with schema integration
  • .gitignore - Updated with Quell-specific entries

Example Files (with --example flag)

  • src/server/example-server.ts - Complete Express server with Quell middleware
  • src/server/schema/example-schema.ts - Sample GraphQL schema with resolvers

Package Configuration

  • package.json - Updated with necessary scripts and dependencies

Generated Dependencies

The CLI automatically installs these packages:

Production Dependencies:

  • express - Web framework
  • graphql - GraphQL implementation
  • redis - Redis client
  • dotenv - Environment variable loader

Development Dependencies:

  • nodemon - Development server with auto-reload
  • typescript - TypeScript compiler
  • ts-node - TypeScript execution engine
  • @types/express - Express type definitions
  • @types/node - Node.js type definitions

Post-Installation Setup

After running the CLI, follow these steps:

1. Configure Redis

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-password

2. Update Your Schema

Replace 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 || "",
});

3. Start Development

# Start the development server
npm run dev

# Visit your GraphQL endpoint
# http://localhost:4000/graphql

Implementation Guide

QuellCache vs QuellRouter

Quell provides two complementary caching strategies:

QuellCache - For Local Schemas

  • 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 })
);

QuellRouter - For 3rd Party APIs

  • 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);

Hybrid Implementation

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.


Rate and Cost Limiting Implementation

@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.


Schema Compatibility Layer

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 });

Usage Notes

Caching Behavior

  • @quell/server reads GraphQL queries from request.body.query and attaches responses to response.locals.queryResponse

  • Cacheable queries must include ID fields (id, _id, Id, or ID) for unique entity identification

Supported operations:

  • Queries with variables, arguments, nested fields, fragments, and aliases
  • Mutations (add/update/delete) with automatic cache invalidation

Non-cacheable operations (executed without caching):

  • 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


Contributors

@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.

Related Documentation

For information on @quell/client, please visit the corresponding README file.

10.0.0

5 months ago

9.0.1

2 years ago

9.0.0

2 years ago

8.0.0

2 years ago

7.0.0

3 years ago

7.0.2

3 years ago

7.0.1

3 years ago

3.0.0

4 years ago

2.3.1

4 years ago

2.2.1

4 years ago

2.2.0

4 years ago

2.1.2

4 years ago

2.1.1

4 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago