Skip to main content

pothos vs TypeGraphQL vs nexus: Code-First GraphQL Schema Builders (2026)

·PkgPulse Team

TL;DR

pothos (formerly GiraphQL) is the most modern code-first GraphQL schema builder — plugin-based, no decorators, excellent TypeScript inference, and has a Prisma plugin that auto-generates types from your database schema. TypeGraphQL uses decorators and class-based definitions — familiar for developers coming from NestJS, works with class-validator for input validation. nexus (from Prisma team) pioneered code-first TypeScript GraphQL but development has slowed — pothos has largely superseded it. For new projects: pothos. For NestJS or decorator-based code: TypeGraphQL. Avoid nexus for new projects (low activity since 2022).

Key Takeaways

  • pothos: ~200K weekly downloads — plugin-based, no decorators, Prisma plugin, best TypeScript DX
  • typegraphql: ~200K weekly downloads — decorator-based, class-validator integration, NestJS-compatible
  • nexus: ~50K weekly downloads — low recent activity, pothos has replaced it
  • Code-first schema builders generate SDL from TypeScript — opposite of SDL-first (schema-first) approach
  • pothos's Prisma plugin generates GraphQL types directly from Prisma schema — minimal boilerplate
  • TypeGraphQL works with both express and Apollo Server v4

SDL-first vs Code-first

SDL-first (schema-first):
  1. Write GraphQL SDL: type Package { name: String!, score: Float! }
  2. Write resolvers to match the SDL
  → Problem: Types can drift, no compile-time safety

Code-first:
  1. Write TypeScript classes/functions
  2. Library generates SDL automatically
  → TypeScript is the source of truth — schema always matches resolvers

Examples:
  SDL-first: graphql-yoga + hand-written typeDefs
  Code-first: pothos, TypeGraphQL, nexus

pothos

pothos — plugin-based code-first GraphQL:

Basic schema

import SchemaBuilder from "@pothos/core"

// Create a schema builder:
const builder = new SchemaBuilder<{
  // Global context type:
  Context: {
    user: User | null
    db: PrismaClient
  }
}>({})

// Define an object type:
const PackageRef = builder.objectType("Package", {
  fields: (t) => ({
    name: t.exposeString("name"),
    weeklyDownloads: t.exposeInt("weeklyDownloads"),
    healthScore: t.exposeFloat("healthScore"),
    tags: t.exposeStringList("tags"),
    isHealthy: t.boolean({
      resolve: (parent) => parent.healthScore >= 80,
    }),
  }),
})

// Define queries:
builder.queryType({
  fields: (t) => ({
    package: t.field({
      type: PackageRef,
      nullable: true,
      args: {
        name: t.arg.string({ required: true }),
      },
      resolve: async (_, { name }, ctx) =>
        ctx.db.package.findUnique({ where: { name } }),
    }),

    packages: t.field({
      type: [PackageRef],
      args: {
        minScore: t.arg.int({ defaultValue: 0 }),
        limit: t.arg.int({ defaultValue: 20 }),
      },
      resolve: async (_, { minScore, limit }, ctx) =>
        ctx.db.package.findMany({
          where: { healthScore: { gte: minScore } },
          take: limit,
          orderBy: { healthScore: "desc" },
        }),
    }),
  }),
})

export const schema = builder.toSchema()
// Generates: SDL + fully-typed resolvers

Prisma plugin (killer feature)

import SchemaBuilder from "@pothos/core"
import PrismaPlugin from "@pothos/plugin-prisma"
import PrismaTypes from "@pothos/plugin-prisma/generated"
import { PrismaClient } from "@prisma/client"

const db = new PrismaClient()

const builder = new SchemaBuilder<{
  PrismaTypes: PrismaTypes  // Generated from your Prisma schema
  Context: { db: PrismaClient }
}>({
  plugins: [PrismaPlugin],
  prisma: { client: db },
})

// Auto-generates all GraphQL types from Prisma schema!
const PackageRef = builder.prismaObject("Package", {
  fields: (t) => ({
    id: t.exposeID("id"),
    name: t.exposeString("name"),
    healthScore: t.exposeFloat("healthScore"),
    weeklyDownloads: t.exposeInt("weeklyDownloads"),
    // Relation — no manual resolver needed:
    dependencies: t.relation("dependencies"),
    maintainers: t.relation("maintainers"),
  }),
})

builder.queryType({
  fields: (t) => ({
    packages: t.prismaField({
      type: ["Package"],
      args: { minScore: t.arg.float({ defaultValue: 0 }) },
      resolve: (query, _, { minScore }, ctx) =>
        ctx.db.package.findMany({
          ...query,  // Pothos includes automatic select optimization
          where: { healthScore: { gte: minScore } },
        }),
    }),
  }),
})

Auth plugin

import SchemaBuilder from "@pothos/core"
import AuthPlugin from "@pothos/plugin-auth-scope"

const builder = new SchemaBuilder<{
  Context: { user: User | null }
  AuthScopes: { authenticated: boolean; admin: boolean }
}>({
  plugins: [AuthPlugin],
  authScopes: async (context) => ({
    authenticated: !!context.user,
    admin: context.user?.role === "admin",
  }),
})

// Protected field:
builder.queryType({
  fields: (t) => ({
    adminStats: t.field({
      type: AdminStats,
      authScopes: { admin: true },  // Only admins can query this
      resolve: async (_, __, ctx) => getAdminStats(ctx),
    }),
  }),
})

TypeGraphQL

TypeGraphQL — decorator-based GraphQL:

Setup with decorators

import "reflect-metadata"  // Required for decorators
import {
  ObjectType,
  Field,
  Resolver,
  Query,
  Arg,
  Ctx,
  Int,
  Float,
  buildSchema,
} from "type-graphql"
import { IsInt, Min, Max } from "class-validator"

// Define GraphQL type with decorators:
@ObjectType()
class Package {
  @Field()
  name: string

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

  @Field(() => Float)
  healthScore: number

  @Field(() => [String])
  tags: string[]

  @Field()
  get isHealthy(): boolean {
    return this.healthScore >= 80
  }
}

// Input type with validation:
@InputType()
class PackageFilterInput {
  @Field(() => Int, { nullable: true, defaultValue: 0 })
  @IsInt()
  @Min(0)
  @Max(100)
  minScore?: number

  @Field(() => Int, { nullable: true, defaultValue: 20 })
  @IsInt()
  @Min(1)
  @Max(100)
  limit?: number
}

// Resolver:
@Resolver(Package)
class PackageResolver {
  @Query(() => Package, { nullable: true })
  async package(
    @Arg("name") name: string,
    @Ctx() ctx: Context
  ): Promise<Package | null> {
    return ctx.db.package.findUnique({ where: { name } })
  }

  @Query(() => [Package])
  async packages(
    @Arg("filter", { nullable: true }) filter: PackageFilterInput = {},
    @Ctx() ctx: Context
  ): Promise<Package[]> {
    return ctx.db.package.findMany({
      where: { healthScore: { gte: filter.minScore ?? 0 } },
      take: filter.limit ?? 20,
    })
  }
}

// Build schema:
const schema = await buildSchema({
  resolvers: [PackageResolver],
  validate: true,  // Enable class-validator
})

Middleware and auth

import { MiddlewareFn, AuthChecker } from "type-graphql"
import { AuthenticationError } from "apollo-server-core"

// Middleware:
export const isAuthenticated: MiddlewareFn<Context> = ({ context }, next) => {
  if (!context.user) throw new AuthenticationError("Not authenticated")
  return next()
}

// Auth checker:
export const authChecker: AuthChecker<Context> = ({ context }, roles) => {
  if (!context.user) return false
  if (roles.length === 0) return true
  return roles.includes(context.user.role)
}

// Apply to resolver:
@Resolver()
class ProtectedResolver {
  @Query(() => String)
  @UseMiddleware(isAuthenticated)
  @Authorized("admin")
  adminSecret(): string {
    return "secret"
  }
}

nexus (legacy reference)

nexus — code-first GraphQL (low activity):

// nexus API — still works but not recommended for new projects
import { makeSchema, objectType, queryType, stringArg, intArg } from "nexus"

const Package = objectType({
  name: "Package",
  definition(t) {
    t.string("name")
    t.int("weeklyDownloads")
    t.float("healthScore")
    t.list.string("tags")
  },
})

const Query = queryType({
  definition(t) {
    t.list.field("packages", {
      type: "Package",
      args: { minScore: intArg({ default: 0 }) },
      resolve: (_, { minScore }, ctx) =>
        ctx.db.package.findMany({ where: { healthScore: { gte: minScore } } }),
    })
  },
})

export const schema = makeSchema({ types: [Package, Query] })
// Works, but: nexus hasn't had major updates since 2022
// Migrate to pothos for new projects

Feature Comparison

FeaturepothosTypeGraphQLnexus
Decorators required
Prisma plugin✅ Official✅ (deprecated)
TypeScript inference✅ Excellent✅ Good✅ Good
Auth plugin
Class-validator
Plugin ecosystem⚠️ Low
NestJS compat
Active maintenance⚠️ Slow
ESM
Learning curveLowMediumMedium

When to Use Each

Choose pothos if:

  • New project with TypeScript and Prisma
  • You prefer functional style over decorator classes
  • You want the best TypeScript inference without reflect-metadata
  • Prisma auto-generated types via @pothos/plugin-prisma

Choose TypeGraphQL if:

  • Your team prefers OOP / decorator-based style
  • Coming from NestJS (TypeGraphQL is the standard there)
  • You want class-validator for automatic input validation
  • You need the largest ecosystem of tutorials and examples for decorator-based GraphQL

Avoid nexus for new projects:

  • Low maintenance activity since 2022
  • Pothos was started by a former Airbnb engineer and has largely replaced it
  • Existing nexus codebases: consider migrating to pothos gradually

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on pothos v4.x, typegraphql v2.x, and nexus v1.x.

Compare GraphQL and TypeScript packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.