@samveys/graphql

Version
1.0.3Nov 5, 2026
Install
pnpm add @samveys/graphql

From packages/graphql/README.md in the framework repository.

A GraphQL endpoint for @samveys/http with limits that run before any resolver. One runtime dependency: graphql (graphql-js).

ProtectionOptionDefaultError extensions.code
DepthmaxDepth10DEPTH_LIMIT
Field count (aliases and fragments expanded)maxCount200COUNT_LIMIT
CostmaxCost, costs, defaultCost1000, 1 per fieldCOST_LIMIT
Rate limit per object type and clientrateLimitsnoneRATE_LIMITED (HTTP 429)
Batching of sensitive operationssensitiveOperations, batching, maxBatchSizeno sensitive ops, batches of up to 10BATCHING_DISABLED
Introspection and the GraphiQL pagenone: on only when app.env === 'local'off elsewhereINTROSPECTION_DISABLED

A refused query gets HTTP 400 with a standard GraphQL errors array, and its resolvers never run. Syntax errors are GRAPHQL_PARSE_FAILED, other validation errors GRAPHQL_VALIDATION_FAILED. Resolver errors that are not GraphQL errors are masked as INTERNAL_SERVER_ERROR outside local.

Set up

ts
import { http } from '@samveys/http'
import { graphql } from '@samveys/graphql'
import { GraphQLInt, GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'

const Report = new GraphQLObjectType({ name: 'Report', fields: { title: { type: GraphQLString } } })

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      reports: {
        type: new GraphQLList(Report),
        args: { first: { type: GraphQLInt } },
        resolve: () => [{ title: 'Q1' }],
      },
    },
  }),
})

export const plugins = [
  http(),
  graphql({
    schema,
    path: '/graphql',
    maxDepth: 8,
    maxCost: 500,
    costs: { 'Query.reports': 10 },
    rateLimits: { Report: { limit: 100, windowMs: 60_000 } },
    sensitiveOperations: ['Mutation.login'],
  }),
]

Cost

A field costs costs['Type.field'], else defaultCost. A list field multiplies the cost of its children by its first or limit argument (a literal or a variable; change the names with listArguments), else by defaultListSize (10). So reports(first: 100) { title } costs 10 + 100 × 1 = 110.

Rate limits

rateLimits counts one hit per request for each listed object type the query touches, per client (ctx.clientIp), in a fixed window. The counter is rateLimitStore, else the 'cache' binding of @samveys/cache (shared across processes), else process memory.

ts
import { createMemoryCounter } from '@samveys/graphql'

export const counter = createMemoryCounter()

Batching

A request body that is an array is a batch. A batch that contains an operation named in sensitiveOperations (by operation name, or by root field such as Mutation.login) is refused whole; the same operation sent alone is fine. batching: false refuses every batch.

Introspection and GraphiQL

Both are available only when the app runs in the local environment. In test, staging and production, __schema and __type are refused, and GET {path} is not routed.

ts
// Over the limit: refused before the resolver runs.
// POST /graphql   { "query": "{ reports(first: 1000) { title } }" }
// 400             { "errors": [{ "message": "Query cost 1010 exceeds the limit of 500", "extensions": { "code": "COST_LIMIT" } }] }