Extensions
Extensions provide hooks into the GraphQL request lifecycle, allowing you to capture timing data, compute metrics, and add metadata to the response’s extensions field.
Extensions vs Middleware
Section titled “Extensions vs Middleware”| Feature | Extensions | Middleware |
|---|---|---|
| Scope | Request lifecycle (parse, validate, execute) | Individual resolver execution |
| Data output | Response extensions field | Transforms resolver result |
| Use case | Tracing, metrics, request info | Auth, logging, caching per-field |
Quick Start
Section titled “Quick Start”-
Register an extension on the builder
import { GraphQLSchemaBuilder, extension, ExtensionsService } from "@effect-gql/core"const builder = GraphQLSchemaBuilder.empty.pipe(extension({name: "tracing",onExecuteStart: () =>Effect.gen(function* () {const ext = yield* ExtensionsServiceyield* ext.set("tracing", { startTime: Date.now() })}),onExecuteEnd: () =>Effect.gen(function* () {const ext = yield* ExtensionsServiceconst data = yield* ext.get()const startTime = (data.tracing as any)?.startTimeyield* ext.merge("tracing", {endTime: Date.now(),durationMs: Date.now() - startTime,})}),}),) -
Build and execute
const schema = builder.buildSchema()const extensions = builder.getExtensions()const result = await Effect.runPromise(execute(schema, serviceLayer, extensions)("{ hello }")) -
Extension data appears in response
{"data": { "hello": "world" },"extensions": {"tracing": {"startTime": 1703001234567,"endTime": 1703001234589,"durationMs": 22}}}
Lifecycle Hooks
Section titled “Lifecycle Hooks”Extensions can hook into four phases of request processing:
extension({ name: "lifecycle",
// Called after parsing succeeds onParse: (source, document) => Effect.Effect<void>,
// Called after validation (with any errors) onValidate: (document, errors) => Effect.Effect<void>,
// Called before resolver execution begins onExecuteStart: (args) => Effect.Effect<void>,
// Called after execution completes (with result) onExecuteEnd: (result) => Effect.Effect<void>,})Hook Parameters
Section titled “Hook Parameters”| Hook | Parameters | Description |
|---|---|---|
onParse | source: string, document: DocumentNode | The query string and parsed AST |
onValidate | document: DocumentNode, errors: readonly GraphQLError[] | The AST and any validation errors |
onExecuteStart | { source, document, variableValues, operationName } | Execution arguments |
onExecuteEnd | result: ExecutionResult | The GraphQL result (data and/or errors) |
Execution Flow
Section titled “Execution Flow”Parse → onParse → Validate → onValidate → onExecuteStart → Execute → onExecuteEndExtensionsService API
Section titled “ExtensionsService API”The ExtensionsService provides methods to write data to the response extensions:
set(key, value)
Section titled “set(key, value)”Set a value, overwriting any existing data for that key:
const ext = yield* ExtensionsServiceyield* ext.set("tracing", { startTime: Date.now() })merge(key, value)
Section titled “merge(key, value)”Deep merge an object into an existing key:
yield* ext.set("tracing", { startTime: 100 })yield* ext.merge("tracing", { endTime: 200 })// Result: { tracing: { startTime: 100, endTime: 200 } }Get all accumulated extension data:
const data = yield* ext.get()const startTime = (data.tracing as any)?.startTimeCommon Patterns
Section titled “Common Patterns”Request Timing
Section titled “Request Timing”Track total request duration:
extension({ name: "timing", onExecuteStart: () => Effect.gen(function* () { const ext = yield* ExtensionsService yield* ext.set("timing", { startTime: Date.now() }) }), onExecuteEnd: () => Effect.gen(function* () { const ext = yield* ExtensionsService const data = yield* ext.get() const timing = data.timing as { startTime: number } yield* ext.merge("timing", { endTime: Date.now(), durationMs: Date.now() - timing.startTime, }) }),})Query Complexity Reporting
Section titled “Query Complexity Reporting”Report complexity metrics in the response:
extension({ name: "complexity", onValidate: (document, errors) => Effect.gen(function* () { if (errors.length > 0) return
// Calculate complexity from AST let complexity = 0 const visit = (selections: any, depth: number) => { for (const sel of selections ?? []) { complexity += depth if (sel.selectionSet) { visit(sel.selectionSet.selections, depth + 1) } } }
for (const def of document.definitions) { if (def.kind === "OperationDefinition") { visit(def.selectionSet?.selections, 1) } }
const ext = yield* ExtensionsService yield* ext.set("complexity", { score: complexity, limit: 1000, }) }),})Request Metadata
Section titled “Request Metadata”Capture information about the request:
extension({ name: "requestInfo", onParse: (source, document) => Effect.gen(function* () { const ext = yield* ExtensionsService yield* ext.set("requestInfo", { queryLength: source.length, operationCount: document.definitions.filter( (d) => d.kind === "OperationDefinition" ).length, }) }), onValidate: (document, errors) => Effect.gen(function* () { const ext = yield* ExtensionsService yield* ext.merge("requestInfo", { validationErrors: errors.length, validated: true, }) }),})Apollo Tracing Compatible
Section titled “Apollo Tracing Compatible”Build Apollo Tracing compatible output:
extension({ name: "apolloTracing", onExecuteStart: () => Effect.gen(function* () { const ext = yield* ExtensionsService yield* ext.set("tracing", { version: 1, startTime: new Date().toISOString(), execution: { resolvers: [] }, }) }), onExecuteEnd: () => Effect.gen(function* () { const ext = yield* ExtensionsService yield* ext.merge("tracing", { endTime: new Date().toISOString(), duration: /* calculated duration in nanoseconds */, }) }),})Multiple Extensions
Section titled “Multiple Extensions”Extensions run in registration order and their data is merged:
const builder = GraphQLSchemaBuilder.empty.pipe( extension({ name: "timing", ... }), extension({ name: "complexity", ... }), extension({ name: "requestInfo", ... }),)Response:
{ "data": { "hello": "world" }, "extensions": { "timing": { "durationMs": 22 }, "complexity": { "score": 5 }, "requestInfo": { "queryLength": 12 } }}Extensions with Services
Section titled “Extensions with Services”Extensions can depend on Effect services:
class TracingService extends Context.Tag("TracingService")< TracingService, { readonly createSpan: (name: string) => Effect.Effect<Span> }>() {}
extension<TracingService>({ name: "openTelemetry", onExecuteStart: (args) => Effect.gen(function* () { const tracing = yield* TracingService const span = yield* tracing.createSpan("graphql.execute") // Store span reference for onExecuteEnd }),})Using with Router
Section titled “Using with Router”When using the HTTP router, extensions are automatically extracted from the builder:
import { toRouter } from "@effect-gql/core/server"
const builder = GraphQLSchemaBuilder.empty.pipe( extension({ name: "timing", ... }), query("hello", { ... }),)
// Extensions are automatically includedconst router = toRouter(builder, serviceLayer)Or with makeGraphQLRouter:
import { makeGraphQLRouter } from "@effect-gql/core/server"
const schema = builder.buildSchema()const extensions = builder.getExtensions()
const router = makeGraphQLRouter(schema, serviceLayer, { extensions,})Full Example
Section titled “Full Example”import { Effect, Layer } from "effect"import * as S from "effect/Schema"import { GraphQLSchemaBuilder, extension, query, execute, ExtensionsService,} from "@effect-gql/core"
const builder = GraphQLSchemaBuilder.empty.pipe( // Tracing extension extension({ name: "tracing", onExecuteStart: () => Effect.gen(function* () { const ext = yield* ExtensionsService yield* ext.set("tracing", { startTime: Date.now() }) }), onExecuteEnd: () => Effect.gen(function* () { const ext = yield* ExtensionsService const data = yield* ext.get() const startTime = (data.tracing as any)?.startTime ?? Date.now() yield* ext.merge("tracing", { endTime: Date.now(), durationMs: Date.now() - startTime, }) }), }),
// Query query("hello", { type: S.String, resolve: () => Effect.succeed("Hello, World!"), }),)
const schema = builder.buildSchema()const extensions = builder.getExtensions()
// Executeconst result = await Effect.runPromise( execute(schema, Layer.empty, extensions)("{ hello }"))
console.log(JSON.stringify(result, null, 2))// {// "data": { "hello": "Hello, World!" },// "extensions": {// "tracing": {// "startTime": 1703001234567,// "endTime": 1703001234589,// "durationMs": 22// }// }// }Next Steps
Section titled “Next Steps”- Middleware - Per-resolver wrapping for cross-cutting concerns
- Error Handling - Handle errors in extensions
- Server Integration - Run your server with extensions