Claude Code for TypeGraphQL: Decorator-Based GraphQL in TypeScript — Claude Skills 360 Blog
Blog / Backend / Claude Code for TypeGraphQL: Decorator-Based GraphQL in TypeScript
Backend

Claude Code for TypeGraphQL: Decorator-Based GraphQL in TypeScript

Published: June 5, 2027
Read time: 6 min read
By: Claude Skills 360

TypeGraphQL builds GraphQL APIs with TypeScript decorators — @ObjectType() class User { @Field() id: string; @Field() email: string } defines a GraphQL type. @Resolver(User) class UserResolver { @Query(() => [User]) async users(@Ctx() ctx: Context) { return ctx.db.users.findMany() } } defines a resolver. @InputType() class CreateUserInput { @Field() @IsEmail() email: string } with class-validator for input validation. @Mutation(() => User) async createUser(@Arg("input") input: CreateUserInput, @Ctx() ctx: Context) adds a mutation. @FieldResolver(() => [Post]) async posts(@Root() user: User, @Ctx() ctx: Context) adds a field resolver for relations. buildSchema({ resolvers: [UserResolver, PostResolver] }) creates the executable schema. @Authorized() or @Authorized("ADMIN") uses the authChecker function for auth guards. Middleware: UseMiddleware(LogAccess) runs functions before resolvers. Claude Code generates TypeGraphQL resolvers, input types, field resolvers with DataLoader, and schema setup.

CLAUDE.md for TypeGraphQL

## TypeGraphQL Stack
- Version: type-graphql >= 2.0, reflect-metadata, class-validator >= 0.14
- tsconfig: "experimentalDecorators": true, "emitDecoratorMetadata": true
- Import reflect-metadata first: import "reflect-metadata" at app entry
- ObjectType: @ObjectType() class Post { @Field() id: string; @Field() title: string }
- Resolver: @Resolver(Post) class PostResolver { @Query(() => [Post]) posts(@Ctx() ctx: Context) { return ctx.db.posts } }
- Input: @InputType() class CreatePost { @Field() @MinLength(5) title: string; @Field() content: string }
- Auth: @Authorized() on query/mutation + authChecker: ({ context }) => context.userId !== null in buildSchema
- Build: const schema = await buildSchema({ resolvers: [PostResolver], authChecker, validate: true })

Type Definitions

// lib/graphql/types.ts — TypeGraphQL ObjectType definitions
import "reflect-metadata"
import {
  ObjectType, Field, ID, Int, Float, InputType,
  registerEnumType,
} from "type-graphql"
import {
  IsEmail, MinLength, MaxLength, IsOptional, IsUrl, ArrayMaxSize,
} from "class-validator"

// Enum
export enum UserRole {
  USER = "user",
  ADMIN = "admin",
}

registerEnumType(UserRole, {
  name: "UserRole",
  description: "User role in the system",
})

// ── Output types ───────────────────────────────────────────────────────────

@ObjectType({ description: "A user account" })
export class User {
  @Field(() => ID)
  id: string

  @Field()
  email: string

  @Field()
  name: string

  @Field(() => UserRole)
  role: UserRole

  @Field({ nullable: true })
  avatarUrl?: string

  @Field()
  createdAt: Date

  // posts is resolved via @FieldResolver — not stored on User directly
}

@ObjectType({ description: "A blog post" })
export class Post {
  @Field(() => ID)
  id: string

  @Field()
  title: string

  @Field()
  slug: string

  @Field()
  content: string

  @Field({ nullable: true })
  excerpt?: string

  @Field()
  published: boolean

  @Field(() => Int)
  readingTime: number

  @Field({ nullable: true })
  publishedAt?: Date

  @Field()
  createdAt: Date

  @Field()
  updatedAt: Date

  // author is resolved via @FieldResolver
}

@ObjectType()
export class PaginatedPosts {
  @Field(() => [Post])
  posts: Post[]

  @Field(() => Int)
  total: number

  @Field(() => Int)
  page: number

  @Field(() => Int)
  pageSize: number

  @Field(() => Int)
  totalPages: number
}

// ── Input types ────────────────────────────────────────────────────────────

@InputType({ description: "Input for creating a new post" })
export class CreatePostInput {
  @Field()
  @MinLength(5, { message: "Title must be at least 5 characters" })
  @MaxLength(200)
  title: string

  @Field()
  @MinLength(20)
  content: string

  @Field({ nullable: true })
  @IsOptional()
  @MaxLength(500)
  excerpt?: string

  @Field(() => Boolean, { defaultValue: false })
  published: boolean = false

  @Field(() => [String], { defaultValue: [] })
  @ArrayMaxSize(10)
  tags: string[] = []
}

@InputType()
export class UpdatePostInput {
  @Field({ nullable: true })
  @IsOptional()
  @MinLength(5)
  @MaxLength(200)
  title?: string

  @Field({ nullable: true })
  @IsOptional()
  @MinLength(20)
  content?: string

  @Field({ nullable: true })
  @IsOptional()
  @MaxLength(500)
  excerpt?: string

  @Field({ nullable: true })
  @IsOptional()
  published?: boolean
}

@InputType()
export class PostsFilter {
  @Field(() => Int, { defaultValue: 1 })
  page: number = 1

  @Field(() => Int, { defaultValue: 10 })
  pageSize: number = 10

  @Field({ nullable: true })
  search?: string

  @Field({ nullable: true })
  tag?: string

  @Field({ nullable: true })
  published?: boolean
}

Resolvers

// lib/graphql/resolvers/PostResolver.ts — TypeGraphQL resolver
import "reflect-metadata"
import {
  Resolver, Query, Mutation, Arg, Ctx, FieldResolver,
  Root, Authorized, Int, ID, UseMiddleware,
} from "type-graphql"
import { Post, PaginatedPosts, CreatePostInput, UpdatePostInput, PostsFilter, User } from "../types"
import type { AppContext } from "../context"
import DataLoader from "dataloader"
import { db } from "@/lib/db"
import { posts, users } from "@/lib/db/schema"
import { eq, ilike, and, count, desc, inArray } from "drizzle-orm"

@Resolver(() => Post)
export class PostResolver {

  // ── Queries ──────────────────────────────────────────────────────────────

  @Query(() => PaginatedPosts, { description: "List posts with pagination" })
  async posts(@Arg("filter", { defaultValue: {} }) filter: PostsFilter): Promise<PaginatedPosts> {
    const { page, pageSize, search, published } = filter
    const offset = (page - 1) * pageSize

    const where = []
    if (search) where.push(ilike(posts.title, `%${search}%`))
    if (published !== undefined && published !== null) {
      where.push(eq(posts.published, published))
    }

    const [result, totalResult] = await Promise.all([
      db.query.posts.findMany({
        where: where.length > 0 ? and(...where) : undefined,
        limit: pageSize,
        offset,
        orderBy: desc(posts.createdAt),
      }),
      db.select({ count: count() }).from(posts),
    ])

    const total = totalResult[0]?.count ?? 0

    return {
      posts: result as Post[],
      total,
      page,
      pageSize,
      totalPages: Math.ceil(total / pageSize),
    }
  }

  @Query(() => Post, { nullable: true })
  async post(@Arg("id", () => ID) id: string): Promise<Post | null> {
    const post = await db.query.posts.findFirst({ where: eq(posts.id, id) })
    return (post as Post) ?? null
  }

  @Query(() => Post, { nullable: true })
  async postBySlug(@Arg("slug") slug: string): Promise<Post | null> {
    const post = await db.query.posts.findFirst({ where: eq(posts.slug, slug) })
    return (post as Post) ?? null
  }

  // ── Field resolvers ──────────────────────────────────────────────────────

  @FieldResolver(() => User, { nullable: true })
  async author(@Root() post: Post, @Ctx() ctx: AppContext): Promise<User | null> {
    // Use DataLoader to batch-load authors
    if (!ctx.userLoader) return null
    return (ctx.userLoader.load((post as any).authorId) as Promise<User | null>)
  }

  @FieldResolver(() => Int)
  readingTime(@Root() post: Post): number {
    return Math.ceil(post.content.split(/\s+/).length / 200)
  }

  // ── Mutations ────────────────────────────────────────────────────────────

  @Authorized()
  @Mutation(() => Post)
  async createPost(
    @Arg("input") input: CreatePostInput,
    @Ctx() ctx: AppContext,
  ): Promise<Post> {
    const slug = input.title
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-|-$/g, "")

    const [post] = await db.insert(posts).values({
      ...input,
      slug,
      authorId: ctx.userId!,
    }).returning()

    return post as Post
  }

  @Authorized()
  @Mutation(() => Post)
  async updatePost(
    @Arg("id", () => ID) id: string,
    @Arg("input") input: UpdatePostInput,
    @Ctx() ctx: AppContext,
  ): Promise<Post> {
    const existing = await db.query.posts.findFirst({ where: eq(posts.id, id) })
    if (!existing) throw new Error("Post not found")
    if ((existing as any).authorId !== ctx.userId) throw new Error("Forbidden")

    const [updated] = await db.update(posts).set(input).where(eq(posts.id, id)).returning()
    return updated as Post
  }

  @Authorized()
  @Mutation(() => Boolean)
  async deletePost(
    @Arg("id", () => ID) id: string,
    @Ctx() ctx: AppContext,
  ): Promise<boolean> {
    const existing = await db.query.posts.findFirst({ where: eq(posts.id, id) })
    if (!existing) throw new Error("Post not found")
    if ((existing as any).authorId !== ctx.userId) throw new Error("Forbidden")

    await db.delete(posts).where(eq(posts.id, id))
    return true
  }
}

Schema Builder

// lib/graphql/schema.ts — build the TypeGraphQL schema
import "reflect-metadata"
import { buildSchema, type AuthChecker } from "type-graphql"
import { PostResolver } from "./resolvers/PostResolver"
import { UserResolver } from "./resolvers/UserResolver"
import type { AppContext } from "./context"

const authChecker: AuthChecker<AppContext> = ({ context }, roles) => {
  if (!context.userId) return false
  if (roles.length === 0) return true
  if (roles.includes("ADMIN") && !context.isAdmin) return false
  return true
}

// In production, build once and reuse
let _schema: Awaited<ReturnType<typeof buildSchema>> | null = null

export async function getSchema() {
  if (_schema) return _schema

  _schema = await buildSchema({
    resolvers: [PostResolver, UserResolver],
    authChecker,
    validate: { forbidUnknownValues: true },
    dateScalarMode: "isoDate",
  })

  return _schema
}

For the Pothos alternative when a functional builder API (non-decorator, no reflect-metadata) with better TypeScript inference, relay plugin for cursor pagination, and more active maintenance is preferred — Pothos is generally recommended for new projects as it doesn’t require experimental decorators, while TypeGraphQL is better for teams already familiar with NestJS or class-based OOP patterns, see the Pothos guide. For the Nexus alternative when nexus.objectType and nexus.makeSchema functional APIs are preferred with a plugin for Prisma integration — Nexus is less actively maintained as of 2025 compared to Pothos, but may be appropriate for teams with existing Nexus codebases, see the Nexus guide. The Claude Skills 360 bundle includes TypeGraphQL skill sets covering resolvers, field resolvers, and schema building. Start with the free tier to try decorator-based GraphQL generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free