iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Resolvers

A resolver is a function that fetches the value for a field. The shape is (parent, args, context, info). Compose resolvers per field; let the executor handle traversal + batching.

Field resolvers, context, DataLoader, errors

EXAMPLE
// 1) Schema
const typeDefs = /* GraphQL */ `
    type User {
        id:    ID!
        name:  String!
        email: String!
        posts: [Post!]!
    }
    type Post {
        id:    ID!
        title: String!
        body:  String!
        author: User!
    }
    type Query {
        users:        [User!]!
        user(id: ID!): User
        posts(authorId: ID): [Post!]!
    }
    type Mutation {
        createUser(input: NewUserInput!): User!
    }
    input NewUserInput {
        name:  String!
        email: String!
    }
`;

// 2) Top-level resolvers
const resolvers = {
    Query: {
        users:  (_, __, ctx) => ctx.db.users.findAll(),
        user:   (_, { id }, ctx) => ctx.db.users.findById(id),
        posts:  (_, { authorId }, ctx) =>
            ctx.db.posts.find({ authorId: authorId ?? undefined }),
    },
    Mutation: {
        createUser: async (_, { input }, ctx) => {
            if (!ctx.user || ctx.user.role !== 'admin') {
                throw new GraphQLError('forbidden', { extensions: { code: 'FORBIDDEN' } });
            }
            return ctx.db.users.create(input);
        },
    },
};

// 3) Field-level resolvers — for relations / computed fields
resolvers.User = {
    // Each User's `posts` field — runs per User
    posts: (user, _, ctx) => ctx.db.posts.findByAuthor(user.id),

    // Computed field
    initials: (user) => user.name.split(' ').map(p => p[0]).join(''),

    // Lazy-load expensive fields only when requested
    avatarUrl: (user, _, ctx) => ctx.s3.getAvatarUrl(user.id),
};

resolvers.Post = {
    author: (post, _, ctx) => ctx.db.users.findById(post.authorId),
};

// 4) Resolver arguments
// (parent, args, context, info)
//   parent  — value from the PARENT field's resolver (the User object for User.posts)
//   args    — arguments from the query (e.g. { id }, { input })
//   context — per-request shared object (db, current user, dataloader, etc.)
//   info    — schema/AST info (rarely needed)

// 5) Context — build per request
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
    context: async ({ req }) => {
        const token = req.headers.authorization?.replace('Bearer ', '');
        const user = token ? await verifyToken(token) : null;
        return {
            user,
            db: createDbClient(),
            loaders: {
                user: new DataLoader(ids => db.users.findByIds(ids)),
                postsByAuthor: new DataLoader(ids => db.posts.findByAuthors(ids)),
            },
        };
    },
});

// 6) The N+1 problem + DataLoader
// query: { users { posts { author { name } } } }
// Without batching: 1 query for users → N for posts → N×M for authors → ✗
//
// DataLoader batches + caches per request:
import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (ids) => {
    const users = await db.users.findByIds(ids);
    const map = new Map(users.map(u => [u.id, u]));
    return ids.map(id => map.get(id));   // MUST return in same order as ids
});

// Then in resolver:
resolvers.Post.author = (post, _, ctx) => ctx.loaders.user.load(post.authorId);
// ALL authors for ALL posts in ONE query.

// 7) Errors
import { GraphQLError } from 'graphql';

resolvers.Query.user = async (_, { id }, ctx) => {
    if (!ctx.user) {
        throw new GraphQLError('Unauthenticated', { extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } } });
    }
    const user = await ctx.db.users.findById(id);
    if (!user) {
        throw new GraphQLError('User not found', { extensions: { code: 'NOT_FOUND' } });
    }
    return user;
};

// 8) Authorisation per resolver
function requireRole(role) {
    return (resolver) => async (parent, args, ctx, info) => {
        if (ctx.user?.role !== role) throw new GraphQLError('Forbidden');
        return resolver(parent, args, ctx, info);
    };
}
resolvers.Mutation.deleteUser = requireRole('admin')(async (_, { id }, ctx) => {
    await ctx.db.users.delete(id);
    return true;
});

// 9) Subscriptions resolver
resolvers.Subscription = {
    postCreated: {
        subscribe: (_, __, ctx) => ctx.pubsub.asyncIterableIterator(['POST_CREATED']),
    },
};

// 10) Default scalars + custom
import { GraphQLScalarType, Kind } from 'graphql';

resolvers.DateTime = new GraphQLScalarType({
    name: 'DateTime',
    description: 'ISO 8601 date-time',
    serialize(value) { return value instanceof Date ? value.toISOString() : value; },
    parseValue(value) { return new Date(value); },
    parseLiteral(ast) { return ast.kind === Kind.STRING ? new Date(ast.value) : null; },
});

// 11) Union / interface — resolveType
resolvers.SearchResult = {
    __resolveType(obj) {
        if (obj.email) return 'User';
        if (obj.title) return 'Post';
        return null;
    },
};

// 12) Default resolver behavior
// If a field on a type has NO explicit resolver, GraphQL returns parent[fieldName] if it exists.
// So you only need resolvers for: relations, computed fields, fields with auth, lazy loading.

// 13) Best practices
//   • Keep resolvers thin — call into services / repos
//   • Use DataLoader for every belongs-to / has-many relationship
//   • Centralise auth via a wrapping HOF (requireRole, requireAuth)
//   • Throw GraphQLError with extensions.code — clients branch on code, not message
//   • Don't return DB rows directly if they contain secrets — pluck what's safe

// 14) Modern alternatives
//   • Code-first (Pothos, Nexus, TypeGraphQL) — types flow from code
//   • Apollo Federation — split schema across services; resolvers on each
//   • Hasura / PostGraphile — auto-generate resolvers from a DB schema

// 15) Testing
import { execute } from 'graphql';
import { buildSchema } from 'graphql';

const result = await execute({
    schema,
    document: parse(`{ user(id: "1") { name posts { title } } }`),
    contextValue: { user: testUser, db: mockDb, loaders: createLoaders(mockDb) },
});
expect(result.data).toMatchObject({ user: { name: 'Ada', posts: [...] } });

Why it matters

DataLoader is non-negotiable for any GraphQL backend with relations. One request, one batched query per relation type — the N+1 problem disappears without changing your schema.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
const resolvers = {
    Query: { user: (_, { id }, ctx) => ctx.db.users.find(id) },
    User:  { posts: (u, _, ctx) => ctx.db.posts.byUser(u.id) },
};
Try it Yourself »

Discussion

Loading…