Skip to content

GraphQL Resolvers ​

This guide covers implementing resolvers for GraphQL queries, mutations, and subscriptions in api.zig.

Overview ​

Resolvers are functions that return data for GraphQL fields. They connect your schema to your data sources.

Basic Resolver ​

zig
const std = @import("std");
const api = @import("api");

// Define resolver context
const ResolverContext = struct {
    allocator: std.mem.Allocator,
    db: *Database,
    user: ?*User,
};

// Simple resolver
fn usersResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    _ = args;
    const users = try ctx.db.getAllUsers();
    return try userListToValue(ctx.allocator, users);
}

// Resolver with arguments
fn userResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const id = args.get("id") orelse return .{ .null = {} };
    const user = try ctx.db.getUserById(id.string) orelse return .{ .null = {} };
    return try userToValue(ctx.allocator, user);
}

Registering Resolvers ​

zig
var executor = api.graphql.Executor.init(allocator, &schema);

// Register field resolvers
try executor.registerResolver("Query", "users", usersResolver);
try executor.registerResolver("Query", "user", userResolver);
try executor.registerResolver("Mutation", "createUser", createUserResolver);
try executor.registerResolver("User", "posts", userPostsResolver);

Query Resolvers ​

List Resolver ​

zig
fn usersResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const limit = args.getInt("limit") orelse 10;
    const offset = args.getInt("offset") orelse 0;
    
    const users = try ctx.db.getUsers(.{
        .limit = @intCast(limit),
        .offset = @intCast(offset),
    });
    
    var list = std.ArrayList(api.graphql.Value).init(ctx.allocator);
    for (users) |user| {
        try list.append(try userToValue(ctx.allocator, user));
    }
    
    return .{ .list = list.items };
}

Single Item Resolver ​

zig
fn userResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const id_val = args.get("id") orelse return .{ .null = {} };
    const id = switch (id_val) {
        .string => |s| s,
        .int => |i| try std.fmt.allocPrint(ctx.allocator, "{d}", .{i}),
        else => return .{ .null = {} },
    };
    
    const user = ctx.db.getUserById(id) catch return .{ .null = {} };
    if (user) |u| {
        return try userToValue(ctx.allocator, u);
    }
    return .{ .null = {} };
}

Search Resolver ​

zig
fn searchUsersResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const query = args.getString("query") orelse return .{ .list = &.{} };
    const limit = args.getInt("limit") orelse 10;
    
    const results = try ctx.db.searchUsers(query, @intCast(limit));
    
    var list = std.ArrayList(api.graphql.Value).init(ctx.allocator);
    for (results) |user| {
        try list.append(try userToValue(ctx.allocator, user));
    }
    
    return .{ .list = list.items };
}

Mutation Resolvers ​

Create Resolver ​

zig
fn createUserResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const input = args.getObject("input") orelse return error.MissingInput;
    
    const name = input.getString("name") orelse return error.MissingName;
    const email = input.getString("email") orelse return error.MissingEmail;
    const role = input.getString("role") orelse "USER";
    
    const user = try ctx.db.createUser(.{
        .name = name,
        .email = email,
        .role = role,
    });
    
    return try userToValue(ctx.allocator, user);
}

Update Resolver ​

zig
fn updateUserResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const id = args.getString("id") orelse return error.MissingId;
    const input = args.getObject("input") orelse return error.MissingInput;
    
    var updates = std.StringHashMap([]const u8).init(ctx.allocator);
    
    if (input.getString("name")) |name| {
        try updates.put("name", name);
    }
    if (input.getString("email")) |email| {
        try updates.put("email", email);
    }
    
    const user = try ctx.db.updateUser(id, updates);
    return try userToValue(ctx.allocator, user);
}

Delete Resolver ​

zig
fn deleteUserResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const id = args.getString("id") orelse return error.MissingId;
    
    const deleted = try ctx.db.deleteUser(id);
    return .{ .boolean = deleted };
}

Field Resolvers ​

Nested Object Resolver ​

zig
fn userPostsResolver(ctx: *ResolverContext, parent: api.graphql.Value, args: anytype) !api.graphql.Value {
    const user_id = parent.getObject().?.getString("id") orelse return .{ .list = &.{} };
    const limit = args.getInt("limit") orelse 10;
    
    const posts = try ctx.db.getPostsByUserId(user_id, @intCast(limit));
    
    var list = std.ArrayList(api.graphql.Value).init(ctx.allocator);
    for (posts) |post| {
        try list.append(try postToValue(ctx.allocator, post));
    }
    
    return .{ .list = list.items };
}

Computed Field Resolver ​

zig
fn userFullNameResolver(ctx: *ResolverContext, parent: api.graphql.Value, args: anytype) !api.graphql.Value {
    _ = args;
    const obj = parent.getObject() orelse return .{ .null = {} };
    
    const first = obj.getString("firstName") orelse "";
    const last = obj.getString("lastName") orelse "";
    
    const full_name = try std.fmt.allocPrint(ctx.allocator, "{s} {s}", .{ first, last });
    return .{ .string = full_name };
}

Subscription Resolvers ​

zig
fn messageAddedResolver(ctx: *ResolverContext, args: anytype) !api.graphql.AsyncIterator {
    const channel_id = args.getString("channelId") orelse return error.MissingChannelId;
    
    return ctx.pubsub.subscribe("message_added", channel_id);
}

fn userStatusChangedResolver(ctx: *ResolverContext, args: anytype) !api.graphql.AsyncIterator {
    _ = args;
    return ctx.pubsub.subscribe("user_status_changed", null);
}

Error Handling ​

Returning Errors ​

zig
fn userResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    const id = args.getString("id") orelse {
        return api.graphql.Value.error("Missing required argument: id", "MISSING_ARGUMENT");
    };
    
    const user = ctx.db.getUserById(id) catch |err| {
        return switch (err) {
            error.NotFound => api.graphql.Value.error("User not found", "NOT_FOUND"),
            error.DatabaseError => api.graphql.Value.error("Database error", "INTERNAL_ERROR"),
            else => error.UnexpectedError,
        };
    };
    
    return try userToValue(ctx.allocator, user);
}

Authorization ​

zig
fn adminOnlyResolver(ctx: *ResolverContext, args: anytype) !api.graphql.Value {
    // Check authentication
    const user = ctx.user orelse {
        return api.graphql.Value.error("Not authenticated", "UNAUTHENTICATED");
    };
    
    // Check authorization
    if (user.role != .ADMIN) {
        return api.graphql.Value.error("Admin access required", "FORBIDDEN");
    }
    
    // Proceed with resolver logic
    return try performAdminAction(ctx, args);
}

Data Loaders ​

Prevent N+1 queries with data loaders:

zig
const UserLoader = api.graphql.DataLoader([]const u8, User);

fn userPostsResolver(ctx: *ResolverContext, parent: api.graphql.Value, args: anytype) !api.graphql.Value {
    _ = args;
    const user_id = parent.getObject().?.getString("id") orelse return .{ .list = &.{} };
    
    // Batch load posts
    const posts = try ctx.post_loader.load(user_id);
    
    var list = std.ArrayList(api.graphql.Value).init(ctx.allocator);
    for (posts) |post| {
        try list.append(try postToValue(ctx.allocator, post));
    }
    
    return .{ .list = list.items };
}

Value Conversion Helpers ​

zig
fn userToValue(allocator: std.mem.Allocator, user: User) !api.graphql.Value {
    var obj = std.StringHashMap(api.graphql.Value).init(allocator);
    
    try obj.put("id", .{ .string = user.id });
    try obj.put("name", .{ .string = user.name });
    try obj.put("email", .{ .string = user.email });
    try obj.put("role", .{ .enum_value = @tagName(user.role) });
    try obj.put("createdAt", .{ .string = try formatDateTime(allocator, user.created_at) });
    
    return .{ .object = obj };
}

fn userListToValue(allocator: std.mem.Allocator, users: []const User) !api.graphql.Value {
    var list = std.ArrayList(api.graphql.Value).init(allocator);
    for (users) |user| {
        try list.append(try userToValue(allocator, user));
    }
    return .{ .list = list.items };
}

See Also ​

Released under the MIT License.