| name | build-graphql-server |
| description | Build a GraphQL server with schema-first design |
| shortcut | gql |
Build GraphQL Server
Create a production-ready GraphQL server with type-safe schemas, optimized resolvers, real-time subscriptions, authentication, and comprehensive tooling using schema-first design principles.
When to Use This Command
Use /build-graphql-server when you need to:
- Build flexible APIs with client-specified queries
- Implement real-time features with subscriptions
- Reduce over-fetching and under-fetching of data
- Create strongly-typed API contracts
- Support multiple client applications with different data needs
- Build microservices with federation support
DON'T use this when:
- Building simple CRUD APIs (REST may be simpler)
- Clients have limited GraphQL knowledge (learning curve)
- Caching is critical and simple (REST caching is more straightforward)
Design Decisions
This command implements Apollo Server with DataLoader as the primary approach because:
- Most mature GraphQL server implementation
- Excellent TypeScript support
- Built-in performance optimizations
- Rich ecosystem of tools and plugins
- Production-proven at scale
- Comprehensive monitoring with Apollo Studio
Alternative considered: GraphQL Yoga
- More lightweight and modular
- Better for serverless deployments
- Newer with smaller ecosystem
- Recommended for edge computing
Alternative considered: Mercurius (Fastify)
- Fastest GraphQL server
- Better for high-performance requirements
- Less mature ecosystem
- Recommended when performance is critical
Prerequisites
Before running this command:
- Define your domain models and relationships
- Choose database and ORM/ODM
- Plan authentication strategy
- Determine subscription requirements
- Identify performance requirements
Implementation Process
Step 1: Design GraphQL Schema
Create comprehensive type definitions with proper nullability and relationships.
Step 2: Implement Resolvers
Build efficient resolvers with DataLoader for batching and caching.
Step 3: Add Authentication & Authorization
Implement context-based auth with field-level permissions.
Step 4: Set Up Subscriptions
Configure WebSocket server for real-time updates.
Step 5: Optimize Performance
Add query complexity analysis, depth limiting, and caching.
Output Format
The command generates:
schema/ - GraphQL schema definitions
resolvers/ - Resolver implementations
dataloaders/ - DataLoader configurations
directives/ - Custom schema directives
server.js - Apollo Server setup
generated/types.ts - TypeScript definitions
tests/ - Integration and unit tests
Code Examples
Example 1: Complete GraphQL Server with Apollo Server
scalar DateTime
scalar Upload
directive @auth(requires: Role = USER) on FIELD_DEFINITION
directive @rateLimit(max: Int, window: String) on FIELD_DEFINITION
directive @deprecated(reason: String) on FIELD_DEFINITION | ENUM_VALUE
enum Role {
USER
MODERATOR
ADMIN
}
type User {
id: ID!
username: String!
email: String!
role: Role!
profile: Profile
posts(
first: Int
after: String
orderBy: PostOrderBy
filter: PostFilter
): PostConnection!
followers: [User!]!
following: [User!]!
createdAt: DateTime!
: !
}
type {
:
:
:
:
}
type {
: !
: !
: !
:
: !
: [!]!
(: , : ): !
: !
: !
: !
:
: !
: !
}
type {
: !
: !
: !
: !
:
: [!]!
: !
: !
: !
}
type {
: !
: !
: !
(: , : ): !
}
# -style pagination types
type {
: [!]!
: !
: !
}
type {
: !
: !
}
type {
: [!]!
: !
: !
}
type {
: !
: !
}
type {
: !
: !
:
:
}
# types
input {
: !
: !
: !
:
}
input {
:
:
:
:
}
input {
: !
: !
: [!]
:
}
input {
:
:
: [!]
:
}
input {
:
:
: [!]
:
}
enum {
}
# types
type {
# queries
: @auth
(: !):
(
:
:
:
): !
# queries
(: !):
(
:
:
:
:
): ! @(: , : )
(: !, : ): [!]!
(: = ): [!]!
# queries
(: !):
: [!]!
}
type {
#
(: !): !
(: !, : !): !
: ! @auth
# mutations
(: !): ! @auth
(: !): ! @auth
(: !): ! @auth
# mutations
(: !): ! @auth
(: !, : !): ! @auth
(: !): ! @(: )
(: !): ! @auth
# mutations
(: !, : !, : ): ! @auth
(: !, : !): ! @auth
(: !): ! @auth
}
type {
# subscriptions
(: ): !
(: !): !
: !
# subscriptions
(: !): !
(: !): !
# activity
(: !): !
}
type {
: !
: !
}
type {
: !
: !
:
}
{ } = ();
{ expressMiddleware } = ();
{ } = ();
{ makeExecutableSchema } = ();
{ } = ();
{ useServer } = ();
express = ();
http = ();
cors = ();
= ();
depthLimit = ();
costAnalysis = ();
typeDefs = ();
resolvers = ();
{ authDirective } = ();
{ rateLimitDirective } = ();
schema = ({
typeDefs,
resolvers
});
schema = (schema, );
schema = (schema, );
app = ();
httpServer = http.(app);
wsServer = ({
: httpServer,
:
});
() {
{
: ( (userIds) => {
users = db..({
: { : { : userIds } }
});
userMap = (users.( [user., user]));
userIds.( userMap.(id));
}),
: ( (postIds) => {
posts = db..({
: { : { : postIds } }
});
postMap = (posts.( [post., post]));
postIds.( postMap.(id));
}),
: ( (postIds) => {
comments = db..({
: { : { : postIds } },
: { : }
});
commentsByPost = {};
comments.( {
(!commentsByPost[comment.]) {
commentsByPost[comment.] = [];
}
commentsByPost[comment.].(comment);
});
postIds.( commentsByPost[id] || []);
})
};
}
serverCleanup = (
{
schema,
: (ctx) => {
{
db,
pubsub,
: (ctx.)
};
}
},
wsServer
);
server = ({
schema,
: [
({ httpServer }),
{
() {
{
() {
serverCleanup.();
}
};
}
}
],
: [
(),
({
: ,
: ,
: ,
: ,
:
})
],
: {
.(error);
(process.. === ) {
error..;
}
{
: error.,
: {
: error..,
: .()
}
};
}
});
() {
server.();
app.(
,
(),
express.(),
(server, {
: ({ req }) => {
token = req..?.(, );
user = token ? (token) : ;
loaders = (db);
{
db,
user,
loaders,
pubsub,
req
};
}
})
);
( httpServer.({ : }, resolve));
.();
}
().(.);
Example 2: Optimized Resolvers with DataLoader
const { GraphQLScalarType } = require('graphql');
const { PubSub, withFilter } = require('graphql-subscriptions');
const DataLoader = require('dataloader');
const pubsub = new PubSub();
const resolvers = {
DateTime: new GraphQLScalarType({
name: 'DateTime',
serialize: (value) => value.toISOString(),
parseValue: (value) => new Date(value),
parseLiteral: (ast) => new Date(ast.value)
}),
Query: {
me: async (_, __, { user, loaders }) => {
if (!user) throw new Error('Not authenticated');
return loaders.userLoader.load(user.id);
},
user: async (_, { id }, { loaders }) => {
loaders..(id);
},
: (_, { first = , after, filter }, { db }) => {
cursor = after ? { : after } : ;
users = db..({
: first + ,
: cursor ? : ,
cursor,
: filter,
: { : }
});
hasNextPage = users. > first;
edges = users.(, first).( ({
: user,
: user.
}));
{
edges,
: {
hasNextPage,
: !!after,
: edges[]?.,
: edges[edges. - ]?.
},
: db..({ : filter })
};
},
: (_, { id }, { loaders }) => {
post = loaders..(id);
(post && !post.) {
canView = (post, user);
(!canView) ;
}
post;
},
: (_, { first = , after, orderBy = , filter }, { db }) => {
where = {};
(filter) {
(filter. !== ) {
where. = filter.;
}
(filter.) {
where. = filter.;
}
(filter.?.) {
where. = {
: {
: { : filter. }
}
};
}
(filter.) {
where. = [
{ : { : filter., : } },
{ : { : filter., : } }
];
}
}
orderByMap = {
: { : },
: { : },
: { : },
: { : },
: { : }
};
cursor = after ? { : after } : ;
posts = db..({
: first + ,
: cursor ? : ,
cursor,
where,
: orderByMap[orderBy],
: {
: {
: { : , : }
}
}
});
hasNextPage = posts. > first;
edges = posts.(, first).( ({
: post,
: post.
}));
{
edges,
: {
hasNextPage,
: !!after,
: edges[]?.,
: edges[edges. - ]?.
},
: db..({ where })
};
},
: (_, { query, first = }, { db }) => {
db..({
: {
: ,
: [
{ : { : query, : } },
{ : { : query, : } },
{ : { : { : { : query, : } } } }
]
},
: first,
: { : }
});
},
: (_, { limit = }, { db }) => {
oneDayAgo = (.() - * * * );
db..({
: {
: ,
: [
{ : { : } },
{ : { : { : { : oneDayAgo } } } }
]
},
: [
{ : },
{ : },
{ : }
],
: limit
});
}
},
: {
: (_, { input }, { db }) => {
existingUser = db..({
: { : input. }
});
(existingUser) {
();
}
hashedPassword = bcrypt.(input., );
user = db..({
: {
: input.,
: input.,
: hashedPassword,
: input. ? {
: input.
} :
},
: { : }
});
token = (user);
{ token, user };
},
: (_, { input }, { db, user, pubsub }) => {
(!user) ();
post = db..({
: {
...input,
: user.,
: input. ? {
: input..( ({
: { : (tag) },
: { : tag, : (tag) }
}))
} :
},
: {
: ,
:
}
});
pubsub.(, { : post });
post;
},
: (_, { id, input }, { db, user }) => {
(!user) ();
post = db..({
: { id }
});
(!post) ();
(post. !== user. && user. !== ) {
();
}
updatedPost = db..({
: { id },
: {
...input,
: input. ? {
: [],
: input..( ({
: { : (tag) },
: { : tag, : (tag) }
}))
} :
},
: {
: ,
:
}
});
pubsub.(, { : updatedPost });
updatedPost;
},
: (_, { postId }, { db, user }) => {
(!user) ();
existingLike = db..({
: {
: {
: user.,
postId
}
}
});
(existingLike) {
db..({
: { : existingLike. }
});
db..({
: { : postId },
: { : { : } },
: { : , : }
});
} {
db..({
: {
: user.,
postId
}
});
db..({
: { : postId },
: { : { : } },
: { : , : }
});
}
}
},
: {
: {
: (
pubsub.([]),
{
(variables.) {
payload.. === variables.;
}
;
}
)
},
: {
: (
pubsub.([]),
{
payload.. === variables.;
}
)
}
},
: {
: (user, { first = , after }, { db }) => {
cursor = after ? { : after } : ;
posts = db..({
: { : user. },
: first + ,
: cursor ? : ,
cursor,
: { : }
});
hasNextPage = posts. > first;
edges = posts.(, first).( ({
: post,
: post.
}));
{
edges,
: {
hasNextPage,
: !!after,
: edges[]?.,
: edges[edges. - ]?.
},
: db..({ : { : user. } })
};
},
: (user, _, { loaders }) => {
loaders..(user.);
},
: (user, _, { loaders }) => {
loaders..(user.);
}
},
: {
: (post, _, { loaders }) => {
loaders..(post.);
},
: (post, { first = , after }, { db }) => {
comments = loaders..(post.);
startIndex = after ? comments.( c. === after) + : ;
paginatedComments = comments.(startIndex, startIndex + first + );
hasNextPage = paginatedComments. > first;
edges = paginatedComments.(, first).( ({
: comment,
: comment.
}));
{
edges,
: {
hasNextPage,
: !!after,
: edges[]?.,
: edges[edges. - ]?.
},
: comments.
};
}
},
: {
: (comment, _, { loaders }) => {
loaders..(comment.);
},
: (comment, _, { loaders }) => {
loaders..(comment.);
}
}
};
. = resolvers;
Example 3: Custom Directives and Performance Optimization
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils');
const { defaultFieldResolver } = require('graphql');
function authDirective(schema, directiveName) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
if (authDirective) {
const { requires } = authDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function (source, args, context, info) {
if (!context.user) {
throw new Error('Not authenticated');
}
if (requires) {
const hasRole = checkUserRole(context.user, requires);
if (!hasRole) {
throw new Error(`Requires ${requires} role`);
}
}
(source, args, context, info);
};
fieldConfig;
}
}
});
}
{ } = ();
rateLimiters = ();
() {
(schema, {
[.]: {
rateLimitDirective = (schema, fieldConfig, directiveName)?.[];
(rateLimitDirective) {
{ max, } = rateLimitDirective;
{ resolve = defaultFieldResolver } = fieldConfig;
key = ;
(!rateLimiters.(key)) {
rateLimiters.(key, ({
: max,
: ()
}));
}
limiter = rateLimiters.(key);
fieldConfig. = () {
userId = context.?. || context..;
{
limiter.(userId);
} (e) {
();
}
(source, args, context, info);
};
fieldConfig;
}
}
});
}
= ();
{ createHash } = ();
{
() {
. = redis;
}
() {
cached = ..(key);
cached ? .(cached) : ;
}
() {
..(key, ttl, .(value));
}
() {
query = info.;
argsString = .(args);
().().();
}
() {
{ ttl = } = options;
(source, args, context, info) => {
(info.. !== ) {
(source, args, context, info);
}
cacheKey = .(info, args);
cached = .(cacheKey);
(cached) {
cached;
}
result = (source, args, context, info);
.(cacheKey, result, ttl);
result;
};
}
}
. = { authDirective, rateLimitDirective, };
Error Handling
| Error | Cause | Solution |
|---|
| "Query depth limit exceeded" | Query too deeply nested | Adjust depth limit or simplify query |
| "Query complexity too high" | Query too expensive | Optimize query or increase complexity budget |
| "N+1 query detected" | Missing DataLoader | Implement DataLoader for relationship |
| "Subscription connection failed" | WebSocket issues | Check WebSocket configuration |
| "Schema validation failed" | Invalid GraphQL schema | Fix schema syntax errors |
Configuration Options
Server Options
introspection: Enable schema introspection (disable in production)
playground: Enable GraphQL Playground
cors: CORS configuration
uploads: File upload support
subscriptions: WebSocket configuration
Performance Options
depthLimit: Maximum query depth (default: 10)
costAnalysis: Query cost limits
dataLoader: Batching configuration
caching: Response caching settings
persistedQueries: APQ support
Best Practices
DO:
- Use DataLoader for all database queries
- Implement proper error handling
- Add field-level authorization
- Version your schema carefully
- Monitor query complexity
- Cache expensive queries
DON'T:
- Expose internal errors to clients
- Allow unlimited query depth
- Ignore N+1 query problems
- Over-fetch in resolvers
- Mix business logic with resolvers
Performance Considerations
- Implement DataLoader for batching and caching
- Use query complexity analysis
- Add response caching with Redis
- Enable persisted queries
- Monitor resolver performance
- Use database query optimization
Security Considerations
- Implement authentication and authorization
- Use query depth limiting
- Add rate limiting
- Validate and sanitize inputs
- Hide internal error details
- Enable CORS appropriately
Related Commands
/api-authentication-builder - Add auth to GraphQL
/api-documentation-generator - Generate GraphQL docs
/api-testing-framework - Test GraphQL APIs
/graphql-federation - Implement microservices
Version History
- v1.0.0 (2024-10): Initial implementation with Apollo Server 4
- Planned v1.1.0: Add federation support for microservices