Extension and utilities for Strapi GraphQL
Buro26’s Strapi GraphQL Extension supercharges your Strapi GraphQL API with:
- Field-level authorization and middleware
- Co-location of schema and resolvers
- Fluent, type-safe builder APIs
- Powerful testing, validation, and tracing utilities
> Supports Strapi v4 and v5
---
``bash`
npm i buro26-strapi-graphql
---
The recommended way to build GraphQL fields is with createGraphQLFieldBuilder().
This fluent, type-safe builder gives you the best DX, composability, and testability.
`typescript
import { createGraphQLFieldBuilder } from 'buro26-strapi-graphql';
import { nonNull, stringArg } from 'nexus';
export default createGraphQLFieldBuilder('Query')
.fieldName('helloWorld')
.args({ name: nonNull(stringArg()) })
.outputType(nonNull('String'))
.description('Returns a personalized greeting')
.resolver(async (parent, args) => Hello World ${args.name})`
.resolverConfig({ auth: false });
---
- Chainable, immutable API for safe, readable code
- Type-safe context and args for resolvers, middleware, and auth
- Built-in support for:
- Middleware and authorization (local and global)
- Argument validation (Zod)
- Tracing/profiling
- Deprecation and description
- Field hooks (before/after resolve)
- Testing in isolation
- Batch/group registration
- Composable presets (.use())
---
Create reusable sets of middleware/auth logic and apply them to any field:
`typescript
const adminPreset = createGraphQLFieldPreset(builder =>
builder
.middleware(requireAdmin)
.authorize(adminAuth)
);
export default createGraphQLFieldBuilder('Query')
.use(adminPreset)
.fieldName('adminHello')
.outputType('String')
.resolver(() => 'Hello, admin!');
`
---
Every field defined with createGraphQLFieldBuilder() or via Nexus’s t.field can use the extensions property to
attach authorization and middleware logic.
- Use the authorize field in extensions to add per-field authorization logic.true
- The function should return (allow), false (deny), or throw an error.
Example:
`typescript`
t.field('myField', {
type: 'String',
extensions: {
authorize: async (parent, args, ctx, info) => {
// Only allow admins
return ctx.state.user?.role === 'admin';
}
},
resolve: () => 'Secret data'
});
> Note:
> If you use the builder, .authorize() will automatically set this property for you.
---
- Use the middlewares field in extensions to add one or more middleware functions to a field.
- Middleware functions wrap the resolver and can perform logic before/after, modify args, or short-circuit the resolver.
Example:
`typescript`
t.field('myField', {
type: 'String',
extensions: {
middlewares: [
next => async (root, args, ctx, info) => {
console.log('Before');
const result = await next(root, args, ctx, info);
console.log('After');
return result;
}
]
},
resolve: () => 'With middleware'
});
> Note:
> If you use the builder, .middleware() will automatically set this property for you.
---
You can register global middleware and global authorization that apply to multiple fields based on type, field
name, or tags.
Important:
> **Global middleware and authorization must be registered _before_ any fields that consume them are built or
registered.**
> This ensures that all relevant fields will pick up the global logic.
Example:
`typescript
import { registerGlobalMiddleware, registerGlobalAuthorize } from 'buro26-strapi-graphql';
// Register global middleware for all admin-tagged Query fields
registerGlobalMiddleware(
({ type, tags }) => type === 'Query' && tags.includes('admin'),
next => async (root, args, ctx, info) => {
// ...admin logic...
return next(root, args, ctx, info);
}
);
// Register global authorization for a specific field
registerGlobalAuthorize(
({ fieldName }) => fieldName === 'deleteUser',
async (parent, args, ctx) => ctx.state.user?.isAdmin
);
// Now define fields that will use these global middlewares/authorizations
export default createGraphQLFieldBuilder('Query')
.fieldName('deleteUser')
.outputType('Boolean')
.tag('admin')
.resolver(() => true);
`
Best Practice:
- Always register global middleware and authorization at the top of your entry file (before any field or extension
exports).
- This guarantees that all fields will be able to consume the global logic.
---
- Use the builder’s .authorize() and .middleware() methods for most use-cases—they automatically set the correct
extensions properties.extensions
- Use the property directly for advanced or manual Nexus usage.
- Register global middleware and authorization before any fields that should use them.
- Use tags to target groups of fields for global logic.
---
Argument validation with Zod:
`typescript
import { z } from 'zod';
const nameSchema = z.object({ name: z.string().min(1) });
export default createGraphQLFieldBuilder('Query')
.fieldName('hello')
.args({ name: 'String!' })
.outputType('String')
.validateArgs(nameSchema)
.resolver((parent, args) => Hello, ${args.name}!);`
Tracing/profiling:
`typescript`
export default createGraphQLFieldBuilder('Query')
.fieldName('profiledHello')
.outputType('String')
.trace() // Pretty console log by default
.resolver(async () => {
await new Promise(r => setTimeout(r, 100));
return 'Hello, profiled!';
});
Testing in isolation:
`typescriptHello, ${args.name}!
const field = createGraphQLFieldBuilder('Query')
.fieldName('hello')
.outputType('String')
.resolver((parent, args, ctx) => );
const result = await field.test({
args: { name: 'Henri' },
context: { user: { id: '123' } }
});
console.log(result); // "Hello, Henri!"
`
---
Register multiple fields at once:
`typescript
import { createGraphQLFieldBuilder, registerFields } from 'buro26-strapi-graphql';
const fieldA = createGraphQLFieldBuilder('Query')
.fieldName('foo')
.outputType('String')
.resolver(() => 'Foo');
const fieldB = createGraphQLFieldBuilder('Query')
.fieldName('bar')
.outputType('String')
.resolver(() => 'Bar');
export default registerFields(fieldA, fieldB);
`
Apply shared logic to a group:
`typescript
import { createFieldGroup } from 'buro26-strapi-graphql';
const requireUser = builder =>
builder.middleware(next => async (root, args, ctx, info) => {
if (!ctx.user) throw new Error('Not authenticated');
return next(root, args, ctx, info);
});
export default createFieldGroup([fieldA, fieldB], requireUser);
`
---
For advanced use-cases, you can use createGraphQLExtension() to build a full extension with custom types, resolvers,
plugins, and more.
`typescript
import { createGraphQLExtension } from 'buro26-strapi-graphql';
import { extendType } from 'nexus';
export default createGraphQLExtension()
.typeDefs(
type HelloWorldResponse {
greeting: String!
}
)`
.types([
extendType({
type: 'Query',
definition(t) {
t.field('helloWorld', {
type: 'HelloWorldResponse',
resolve: () => ({ greeting: 'Hello, world!' })
});
}
})
])
.resolvers({
Query: {
helloWorld: () => ({ greeting: 'Hello, world!' })
}
});
> Note:
> For most use-cases, prefer the field builder.
> Use the extension builder for advanced scenarios (custom plugins, full type overrides, etc).
---
You can override or extend fields in existing types using the overrideField helper:
`typescript
import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
import { extendType } from 'nexus';
export default createGraphQLExtension(strapi => ({
types() {
return [
extendType({
type: 'MyType',
definition(t) {
overrideField
contentTypeName: 'api::my-type.my-type',
fieldName: 'myField',
authorize: async (root, args, context) => {
// Authorization logic
return true;
},
resolve: async (root) => {
// Resolver is optional
return false;
}
});
}
})
];
}
}));
`
---
This package provides a set of utility functions to make working with Strapi GraphQL resolvers easier and more typesafe:
- resolveEntity
Resolve a single entity by ID.
`typescript
import { resolveEntity } from 'buro26-strapi-graphql';
// Inside a resolver:
return resolveEntity
args: { id: 'the id here' },
parent,
context,
info,
});
`
- resolveEntityRelation
Resolve a related entity from a relation field.
`typescript
import { resolveEntityRelation } from 'buro26-strapi-graphql';
return resolveEntityRelation
'api::my-content-type.my-content-type',
'relationFieldName',
{ args, parent, context, info }
);
`
- resolveEntityCollection
Resolve a collection of entities.
`typescript
import { createEntityCollectionResolver } from 'buro26-strapi-graphql';
const resolver = createEntityCollectionResolver
return resolver({ args, parent, context, info });
`
- resolveEntityRelationCollection
Resolve a collection of related entities from a relation field.
`typescript
import { resolveEntityRelationCollection } from 'buro26-strapi-graphql';
return resolveEntityRelationCollection
'api::my-content-type.my-content-type',
'relationFieldName',
{ args, parent, context, info }
);
`
- toEntityResponse
Typesafe builder for a single entity response.
`typescript
import { toEntityResponse } from 'buro26-strapi-graphql';
return toEntityResponse(entity, 'api::my-content-type.my-content-type');
`
- toEntityCollectionResponse
Typesafe builder for a collection response.
`typescript
import { toEntityCollectionResponse } from 'buro26-strapi-graphql';
return toEntityCollectionResponse(entities, 'api::my-content-type.my-content-type');
`
- resolveArgs
Typesafe utility to transform GraphQL args for use with Strapi’s entity manager.
`typescript
import { resolveArgs } from 'buro26-strapi-graphql';
const entityManagerArgs = resolveArgs(graphqlArgs);
`
- shadowCRUD
Typesafe utility to create shadow CRUD operations for a content type.
`typescript
import { shadowCRUD } from 'buro26-strapi-graphql';
shadowCRUD('api::article.article')
.disableActions(['create', 'update', 'delete']);
shadowCRUD('api::category.category')
.field('title')
.disable();
`
---
To run tests:
`bash``
bun test
---
Pull requests and contributions are welcome!
See the repo for details.
---
Buro26 – https://buro26.digital
Special thanks to all contributors and the open-source community for their support and contributions.
---
This project is licensed under the MIT License - see the LICENSE file for details.
---
Active development.
Check issues for updates and planned features.
---
Questions or suggestions?
Open an issue or reach out to Buro26.