buro26-strapi-graphql
v0.1.0
Published
Extension and utilities for Strapi GraphQL
Readme
Strapi GraphQL Extension
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
🚀 Getting Started
npm i buro26-strapi-graphql🏗️ Recommended: The Field Builder Pattern
The recommended way to build GraphQL fields is with createGraphQLFieldBuilder().
This fluent, type-safe builder gives you the best DX, composability, and testability.
Example: A Custom Query Field
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 });Why use the Field Builder?
- 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())
Composable Presets
Create reusable sets of middleware/auth logic and apply them to any field:
const adminPreset = createGraphQLFieldPreset(builder =>
builder
.middleware(requireAdmin)
.authorize(adminAuth)
);
export default createGraphQLFieldBuilder('Query')
.use(adminPreset)
.fieldName('adminHello')
.outputType('String')
.resolver(() => 'Hello, admin!');🔒 Field-Level Authorization and Middleware
Every field defined with createGraphQLFieldBuilder() or via Nexus’s t.field can use the extensions property to
attach authorization and middleware logic.
Field-Level Authorization
- Use the
authorizefield inextensionsto add per-field authorization logic. - The function should return
true(allow),false(deny), or throw an error.
Example:
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.
Field-Level Middleware
- Use the
middlewaresfield inextensionsto 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:
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.
🌍 Global Middleware and Authorization
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:
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.
📝 Best Practices
- Use the builder’s
.authorize()and.middleware()methods for most use-cases—they automatically set the correctextensionsproperties. - Use the
extensionsproperty 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.
Validation, Tracing, and Testing
Argument validation with Zod:
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:
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:
const field = createGraphQLFieldBuilder('Query')
.fieldName('hello')
.outputType('String')
.resolver((parent, args, ctx) => `Hello, ${args.name}!`);
const result = await field.test({
args: { name: 'Henri' },
context: { user: { id: '123' } }
});
console.log(result); // "Hello, Henri!"Batch Registration and Field Groups
Register multiple fields at once:
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:
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);🛠️ Advanced: The Extension Builder
For advanced use-cases, you can use createGraphQLExtension() to build a full extension with custom types, resolvers,
plugins, and more.
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).
🧩 Overriding and Extending Fields
You can override or extend fields in existing types using the overrideField helper:
import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
import { extendType } from 'nexus';
export default createGraphQLExtension(strapi => ({
types() {
return [
extendType({
type: 'MyType',
definition(t) {
overrideField<MyContentType>(t, {
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;
}
});
}
})
];
}
}));🧰 Utility Functions
This package provides a set of utility functions to make working with Strapi GraphQL resolvers easier and more typesafe:
Entity and Collection Resolvers
resolveEntity
Resolve a single entity by ID.import { resolveEntity } from 'buro26-strapi-graphql'; // Inside a resolver: return resolveEntity<MyContentType>('api::my-content-type.my-content-type', { args: { id: 'the id here' }, parent, context, info, });resolveEntityRelation
Resolve a related entity from a relation field.import { resolveEntityRelation } from 'buro26-strapi-graphql'; return resolveEntityRelation<MyContentType>( 'api::my-content-type.my-content-type', 'relationFieldName', { args, parent, context, info } );resolveEntityCollection
Resolve a collection of entities.import { createEntityCollectionResolver } from 'buro26-strapi-graphql'; const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type'); return resolver({ args, parent, context, info });resolveEntityRelationCollection
Resolve a collection of related entities from a relation field.import { resolveEntityRelationCollection } from 'buro26-strapi-graphql'; return resolveEntityRelationCollection<MyContentType>( 'api::my-content-type.my-content-type', 'relationFieldName', { args, parent, context, info } );
Response Builders
toEntityResponse
Typesafe builder for a single entity response.import { toEntityResponse } from 'buro26-strapi-graphql'; return toEntityResponse(entity, 'api::my-content-type.my-content-type');toEntityCollectionResponse
Typesafe builder for a collection response.import { toEntityCollectionResponse } from 'buro26-strapi-graphql'; return toEntityCollectionResponse(entities, 'api::my-content-type.my-content-type');
Argument and CRUD Utilities
resolveArgs
Typesafe utility to transform GraphQL args for use with Strapi’s entity manager.import { resolveArgs } from 'buro26-strapi-graphql'; const entityManagerArgs = resolveArgs(graphqlArgs);shadowCRUD
Typesafe utility to create shadow CRUD operations for a content type.import { shadowCRUD } from 'buro26-strapi-graphql'; shadowCRUD('api::article.article') .disableActions(['create', 'update', 'delete']); shadowCRUD('api::category.category') .field('title') .disable();
🧪 Testing
To run tests:
bun test🤝 Contributing
Pull requests and contributions are welcome!
See the repo for details.
👥 Authors and Acknowledgment
Buro26 – https://buro26.digital
Special thanks to all contributors and the open-source community for their support and contributions.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🚦 Project Status
Active development.
Check issues for updates and planned features.
Questions or suggestions?
Open an issue or reach out to Buro26.
