Skip to main content
Version: Next (2.x)

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 add @baeta/complexity

Basic setup

1. Create the app plugin

createComplexity returns:

  • complexity — a helper for $use to override settings on a type or field.
  • complexityAppPlugin — register this on createApplication.
src/lib/complexity.ts
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

src/app.ts
import { createApplication } from "@baeta/core";
import { complexityAppPlugin } from "./lib/complexity.ts";
import modules from "./modules/index.ts";

const baeta = createApplication({
modules,
plugins: [complexityAppPlugin],
});
note

Forgetting to register complexityAppPlugin is a build-time error: if any module uses complexity(...) without the plugin, Baeta throws at schema build.

tip

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

  1. Every field gets a base complexity (default: 1).
  2. List fields multiply their subtree by the list multiplier.
  3. Nested costs add up.
  4. The total is compared against the configured complexity limit; 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