Query Complexity
@baeta/complexity scores every incoming query and rejects requests that exceed your configured budget. The default scoring is "1 per field, list multiplier on list types" — override either globally or per field as needed.
At a glance
- Automatic complexity scoring for every field.
- Per-field overrides via
$use(complexity(...)). - Limits computed dynamically from
ctx(e.g. higher for authenticated users). - List multiplier reflects breadth across
[T]fields. - Hard caps on query depth and breadth.
Installation
- yarn
- npm
- pnpm
- bun
yarn add @baeta/complexity
npm install @baeta/complexity
pnpm add @baeta/complexity
bun add @baeta/complexity
Basic setup
1. Create the app plugin
createComplexity returns:
complexity— a helper for$useto override settings on a type or field.complexityAppPlugin— register this oncreateApplication.
import { createComplexity } from "@baeta/complexity";
import type { Context } from "../types/context.ts";
export const { complexity, complexityAppPlugin } = createComplexity<Context>({
// Default complexity score for fields
defaultComplexity: 1,
// Multiplier applied to list fields
defaultListMultiplier: 10,
// Dynamic limits based on context
async limit(ctx) {
return {
depth: 10, // Maximum query depth
breadth: 50, // Maximum number of fields at each level
complexity: 1000, // Maximum total complexity score
};
},
// Alternatively, use static limits
// limit: {
// depth: 10,
// breadth: 50,
// complexity: 1000,
// }
});
2. Register the app plugin
import { createApplication } from "@baeta/core";
import { complexityAppPlugin } from "./lib/complexity.ts";
import modules from "./modules/index.ts";
const baeta = createApplication({
modules,
plugins: [complexityAppPlugin],
});
Forgetting to register complexityAppPlugin is a build-time error: if any module uses complexity(...) without the plugin, Baeta throws at schema build.
Register authAppPlugin before complexityAppPlugin for most APIs — unauthenticated requests bounce before any cost calculation runs. Complexity-first saves I/O on expensive-but-doomed queries but leaks schema shape to anonymous callers. See App Plugins for the full tradeoff.
Customizing the rules
Per-field overrides
Chain $use(complexity(...)) on a field or type to override the defaults:
import { complexity } from "../../lib/complexity.ts";
import { UserModule } from "./typedef.ts";
const { Query, User } = UserModule;
// Custom complexity for a type — chain into $fields
const userResolver = User
.$use(
complexity(() => ({
complexity: 2,
})),
)
.$fields({
id: User.id.key("id"),
email: User.email.key("email"),
});
// Disable complexity for a specific field
const userQuery = Query.user
.$use(complexity(() => false))
.resolve(({ args }) => {
return findUser(args.where);
});
// Custom complexity score for a list field
const usersQuery = Query.users
.$use(
complexity(({ args, ctx }) => ({
complexity: 1,
multiplier: 5,
})),
)
.resolve(() => {
return findUsers();
});
Dynamic rules
Compute the cost from ctx or args:
const usersQuery = Query.users
.$use(
complexity(({ args, ctx }) => {
return {
complexity: 1,
multiplier: args.limit,
};
}),
)
.resolve(() => {
return findUsers();
});
How the score is calculated
- Every field gets a base complexity (default:
1). - List fields multiply their subtree by the list multiplier.
- Nested costs add up.
- The total is compared against the configured
complexitylimit; over-budget queries fail.
Example:
query {
users(first: 10) {
# complexity: 1 * 10 (multiplier)
name # complexity: 1 * 10 (inherited multiplier)
posts {
# complexity: 1 * 10 * 10 (nested list)
title # complexity: 1 * 10 * 10 (inherited multiplier)
}
}
}
# Total complexity: 210