pothos vs TypeGraphQL vs nexus: Code-First GraphQL Schema Builders (2026)
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
| Feature | pothos | TypeGraphQL | nexus |
|---|---|---|---|
| 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 curve | Low | Medium | Medium |
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.