# How to generate typed APIs with Cursor

- Tool: Cursor
- Difficulty: Beginner
- Time required: 10-15 min
- Compatibility: Cursor Free+, TypeScript with GraphQL (Apollo, Mercurius, or yoga)
- Last updated: March 2026

## TL;DR

Cursor generates untyped GraphQL resolvers by default because it lacks context about your schema and codegen types. By referencing generated types with @file, adding GraphQL rules to .cursorrules, and using a schema-first workflow, you get resolvers with full type safety, proper error handling, and context typing that match your GraphQL schema exactly.

## Generating typed GraphQL APIs with Cursor

GraphQL resolvers need precise typing to match the schema contract. Without codegen type references, Cursor generates resolvers with `any` types, incorrect return shapes, and missing nullable handling. This tutorial shows how to connect Cursor to your GraphQL type generation output so every resolver it creates is fully type-safe.

## Before you start

- Cursor installed with a TypeScript + GraphQL project
- GraphQL Code Generator configured (graphql-codegen)
- A GraphQL schema defined (.graphql files)
- Generated TypeScript types from codegen

## Step-by-step guide

### 1. Generate types from your schema

Run GraphQL Code Generator to produce TypeScript types from your schema. These generated types are what Cursor needs to create properly typed resolvers. Cursor cannot read .graphql schema files directly for type inference.

```
# codegen.yml
overwrite: true
schema: "src/schema/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-resolvers
    config:
      contextType: "../context#GraphQLContext"
      mapperTypeSuffix: Model
      useIndexSignature: true
```

> Pro tip: Configure contextType in codegen.yml so generated resolver types include your authenticated context shape. This gives Cursor full visibility into context.user and context.db.

**Expected result:** Generated TypeScript types at src/generated/graphql.ts with resolver type definitions.

### 2. Add GraphQL rules to .cursor/rules

Create rules that tell Cursor to always import from the generated types file and follow your resolver conventions.

```
---
description: GraphQL resolver conventions
globs: "src/resolvers/**/*.ts"
alwaysApply: true
---

## GraphQL Resolver Rules
- ALWAYS import resolver types from @/generated/graphql
- ALWAYS type resolvers using generated Resolvers type or individual QueryResolvers, MutationResolvers
- ALWAYS type the context parameter as GraphQLContext from @/context
- Handle nullable fields: return null explicitly, not undefined
- Use DataLoader for N+1 prevention in nested resolvers
- Throw GraphQLError for user-facing errors with proper extensions
- NEVER use 'any' type in resolver arguments or return values
- Validate mutation inputs before processing
```

**Expected result:** Cursor follows typed GraphQL resolver conventions whenever generating resolver code.

### 3. Generate a typed query resolver

Ask Cursor to generate a resolver by referencing the generated types and your schema. The AI will use the exact types from codegen, ensuring the resolver matches the schema contract.

```
// Cursor Chat prompt (Cmd+L):
// @src/generated/graphql.ts @src/schema/user.graphql
// @src/context.ts
// Generate the User query resolvers: user(id: ID!) and
// users(page: Int, limit: Int). Use the generated
// QueryResolvers type. Access the database through
// context.db. Return types must match the schema exactly.

import type { QueryResolvers } from '@/generated/graphql';
import { GraphQLError } from 'graphql';

export const userQueries: QueryResolvers = {
  user: async (_parent, { id }, context) => {
    const user = await context.db.user.findUnique({ where: { id } });
    if (!user) {
      throw new GraphQLError('User not found', {
        extensions: { code: 'NOT_FOUND' },
      });
    }
    return user;
  },
  users: async (_parent, { page = 1, limit = 20 }, context) => {
    return context.db.user.findMany({
      skip: (page - 1) * limit,
      take: limit,
    });
  },
};
```

**Expected result:** Typed query resolvers that match the schema with proper context access and error handling.

### 4. Generate a typed mutation resolver

Mutations need input validation and proper error handling. Ask Cursor to generate mutation resolvers with authentication checks and input validation.

```
// Cursor Chat prompt (Cmd+L):
// @src/generated/graphql.ts @src/context.ts
// Generate the createUser and updateUser mutation resolvers.
// Check that context.user exists (authenticated).
// Validate email format. Use the generated MutationResolvers type.

import type { MutationResolvers } from '@/generated/graphql';
import { GraphQLError } from 'graphql';

export const userMutations: MutationResolvers = {
  createUser: async (_parent, { input }, context) => {
    if (!context.user) {
      throw new GraphQLError('Authentication required', {
        extensions: { code: 'UNAUTHENTICATED' },
      });
    }
    if (!input.email.includes('@')) {
      throw new GraphQLError('Invalid email format', {
        extensions: { code: 'BAD_USER_INPUT' },
      });
    }
    return context.db.user.create({ data: input });
  },
};
```

**Expected result:** Typed mutation resolvers with authentication, validation, and proper GraphQL error codes.

### 5. Generate nested field resolvers

For relationships like User.posts, generate field resolvers that prevent N+1 queries. Reference the generated types and mention DataLoader if available.

```
// Cursor Chat prompt (Cmd+L):
// @src/generated/graphql.ts Generate the User field
// resolver for 'posts' that loads the user's posts.
// Use context.loaders.postsByUserId DataLoader to
// prevent N+1 queries. Type it using UserResolvers.

import type { UserResolvers } from '@/generated/graphql';

export const userFieldResolvers: UserResolvers = {
  posts: async (parent, _args, context) => {
    return context.loaders.postsByUserId.load(parent.id);
  },
  fullName: (parent) => {
    return `${parent.firstName} ${parent.lastName}`;
  },
};
```

**Expected result:** Field resolvers with DataLoader integration and computed fields, fully typed from codegen.

## Complete code example

File: `src/resolvers/user.queries.ts`

```typescript
import type { QueryResolvers } from '@/generated/graphql';
import { GraphQLError } from 'graphql';

export const userQueries: QueryResolvers = {
  user: async (_parent, { id }, context) => {
    if (!id) {
      throw new GraphQLError('User ID is required', {
        extensions: { code: 'BAD_USER_INPUT' },
      });
    }

    const user = await context.db.user.findUnique({
      where: { id },
    });

    if (!user) {
      throw new GraphQLError(`User not found: ${id}`, {
        extensions: { code: 'NOT_FOUND' },
      });
    }

    return user;
  },

  users: async (_parent, args, context) => {
    const page = Math.max(1, args.page ?? 1);
    const limit = Math.min(100, Math.max(1, args.limit ?? 20));

    const [items, total] = await Promise.all([
      context.db.user.findMany({
        skip: (page - 1) * limit,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      context.db.user.count(),
    ]);

    return {
      items,
      total,
      page,
      limit,
      hasNext: page * limit < total,
    };
  },

  me: async (_parent, _args, context) => {
    if (!context.user) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' },
      });
    }
    return context.db.user.findUnique({
      where: { id: context.user.id },
    });
  },
};
```

## Common mistakes

- **Not referencing the generated types file in Cursor prompts** — Without @src/generated/graphql.ts, Cursor invents its own types that may not match your actual schema, causing runtime type mismatches. Fix: Always include @src/generated/graphql.ts in GraphQL resolver prompts.
- **Returning undefined instead of null for nullable fields** — GraphQL distinguishes between null (valid absent value) and undefined (field not resolved). Returning undefined causes unexpected behavior in clients. Fix: Add to .cursorrules: 'Return null explicitly for absent nullable fields, never undefined.'
- **Skipping DataLoader for nested relationship resolvers** — Without DataLoader, each nested field resolver makes a separate database query, causing N+1 performance issues. Fix: Mention DataLoader in your prompt for any resolver that loads related entities.

## Best practices

- Run graphql-codegen before generating resolvers so types are current
- Always reference @src/generated/graphql.ts in resolver generation prompts
- Configure contextType in codegen.yml for typed context parameters
- Use DataLoader for all nested relationship resolvers to prevent N+1 queries
- Throw GraphQLError with proper extension codes (UNAUTHENTICATED, NOT_FOUND, BAD_USER_INPUT)
- Validate mutation inputs at the resolver level before calling services
- Split resolvers into queries, mutations, and field resolvers for maintainability

## Frequently asked questions

### Do I need graphql-codegen to generate typed resolvers?

Strongly recommended. Without codegen, Cursor generates approximate types that may drift from your actual schema. Codegen ensures exact type parity between schema and resolvers.

### Can Cursor generate the GraphQL schema itself?

Yes. Ask Cursor to generate .graphql schema files, then run codegen to produce types. However, start with the schema and generate resolvers from it, not the reverse.

### How do I handle file uploads in Cursor-generated resolvers?

Ask Cursor to use graphql-upload for file handling. Reference the Upload scalar in your schema and specify the upload processing logic in your prompt.

### Will Cursor handle subscription resolvers?

Yes. Specify your pubsub implementation (e.g., graphql-subscriptions) and ask Cursor to generate subscribe resolvers with proper typing and topic filtering.

### How do I generate resolvers for a federated GraphQL schema?

Add federation directives to your codegen config and reference the generated types. Tell Cursor about your federation setup: 'This is a federated subgraph. Use __resolveReference for entity types.'

---

Source: https://www.rapidevelopers.com/cursor-tutorial/how-to-get-cursor-ai-to-generate-strongly-typed-graphql-resolvers-in-typescript
© RapidDev — https://www.rapidevelopers.com/cursor-tutorial/how-to-get-cursor-ai-to-generate-strongly-typed-graphql-resolvers-in-typescript
