GraphQL
GraphQL with TypeScript
GraphQL with TypeScript
Build type-safe GraphQL servers and clients using TypeScript for end-to-end type safety.
Type-Safe Resolvers
Define TypeScript types for your resolvers:
// types/graphql.ts
export interface User {
id: string;
name: string;
email: string;
posts?: Post[];
}
export interface Post {
id: string;
title: string;
content: string;
authorId: string;
}
export interface Context {
db: Database;
user?: User;
}
export interface CreateUserInput {
name: string;
email: string;
}
// resolvers.ts
import { Resolvers } from './generated/graphql';
import { Context } from './types/graphql';
export const resolvers: Resolvers<Context> = {
Query: {
user: async (_, { id }, { db }) => {
const user = await db.user.findUnique({ where: { id } });
if (!user) throw new Error('User not found');
return user;
},
users: async (_, __, { db }) => {
return db.user.findMany();
},
},
Mutation: {
createUser: async (_, { input }, { db }) => {
return db.user.create({
data: {
name: input.name,
email: input.email,
},
});
},
},
User: {
posts: async (parent, _, { db }) => {
return db.post.findMany({
where: { authorId: parent.id },
});
},
},
Post: {
author: async (parent, _, { db }) => {
const author = await db.user.findUnique({
where: { id: parent.authorId },
});
if (!author) throw new Error('Author not found');
return author;
},
},
};
Generated Types
Use GraphQL Code Generator for automatic type generation:
# codegen.yml for server
schema: ./schema.graphql
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
config:
contextType: ../types/graphql#Context
mappers:
User: ../types/graphql#User
Post: ../types/graphql#Post
// Generated resolver types
export type ResolversTypes = {
User: ResolverTypeWrapper<User>;
String: ResolverTypeWrapper<Scalars['String']>;
Post: ResolverTypeWrapper<Post>;
Query: ResolverTypeWrapper<{}>;
Mutation: ResolverTypeWrapper<{}>;
CreateUserInput: CreateUserInput;
};
export type UserResolvers<
ContextType = Context,
ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']
> = {
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
email?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
posts?: Resolver<Array<ResolversTypes['Post']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
Tip: Map GraphQL types to your database models using the
mappers configuration to ensure type consistency.
Nexus - Code-First Approach
Build your GraphQL schema with code using Nexus:
import { objectType, extendType, stringArg, nonNull, inputObjectType } from 'nexus';
// Define User type
export const User = objectType({
name: 'User',
definition(t) {
t.nonNull.id('id');
t.nonNull.string('name');
t.nonNull.string('email');
t.nonNull.list.nonNull.field('posts', {
type: 'Post',
resolve: (parent, _, ctx) => {
return ctx.db.post.findMany({
where: { authorId: parent.id },
});
},
});
},
});
// Define Post type
export const Post = objectType({
name: 'Post',
definition(t) {
t.nonNull.id('id');
t.nonNull.string('title');
t.nonNull.string('content');
t.nonNull.field('author', {
type: 'User',
resolve: (parent, _, ctx) => {
return ctx.db.user.findUniqueOrThrow({
where: { id: parent.authorId },
});
},
});
},
});
// Define input types
export const CreateUserInput = inputObjectType({
name: 'CreateUserInput',
definition(t) {
t.nonNull.string('name');
t.nonNull.string('email');
},
});
// Define queries
export const UserQuery = extendType({
type: 'Query',
definition(t) {
t.field('user', {
type: 'User',
args: {
id: nonNull(stringArg()),
},
resolve: (_, { id }, ctx) => {
return ctx.db.user.findUnique({ where: { id } });
},
});
t.nonNull.list.nonNull.field('users', {
type: 'User',
resolve: (_, __, ctx) => {
return ctx.db.user.findMany();
},
});
},
});
// Define mutations
export const UserMutation = extendType({
type: 'Mutation',
definition(t) {
t.nonNull.field('createUser', {
type: 'User',
args: {
input: nonNull('CreateUserInput'),
},
resolve: (_, { input }, ctx) => {
return ctx.db.user.create({
data: {
name: input.name,
email: input.email,
},
});
},
});
},
});
// schema.ts
import { makeSchema } from 'nexus';
import * as types from './graphql';
import path from 'path';
export const schema = makeSchema({
types,
outputs: {
schema: path.join(__dirname, './generated/schema.graphql'),
typegen: path.join(__dirname, './generated/nexus-typegen.ts'),
},
contextType: {
module: path.join(__dirname, './context.ts'),
export: 'Context',
},
});
TypeGraphQL
Use decorators for schema definition with TypeGraphQL:
import { ObjectType, Field, ID, Resolver, Query, Mutation, Arg, Ctx } from 'type-graphql';
@ObjectType()
class User {
@Field(() => ID)
id: string;
@Field()
name: string;
@Field()
email: string;
@Field(() => [Post])
posts: Post[];
}
@ObjectType()
class Post {
@Field(() => ID)
id: string;
@Field()
title: string;
@Field()
content: string;
@Field(() => User)
author: User;
}
@InputType()
class CreateUserInput {
@Field()
name: string;
@Field()
email: string;
}
@Resolver(User)
class UserResolver {
@Query(() => User, { nullable: true })
async user(@Arg('id') id: string, @Ctx() ctx: Context): Promise<User | null> {
return ctx.db.user.findUnique({ where: { id } });
}
@Query(() => [User])
async users(@Ctx() ctx: Context): Promise<User[]> {
return ctx.db.user.findMany();
}
@Mutation(() => User)
async createUser(
@Arg('input') input: CreateUserInput,
@Ctx() ctx: Context
): Promise<User> {
return ctx.db.user.create({
data: {
name: input.name,
email: input.email,
},
});
}
@FieldResolver(() => [Post])
async posts(@Root() user: User, @Ctx() ctx: Context): Promise<Post[]> {
return ctx.db.post.findMany({
where: { authorId: user.id },
});
}
}
// server.ts
import 'reflect-metadata';
import { ApolloServer } from 'apollo-server';
import { buildSchema } from 'type-graphql';
async function bootstrap() {
const schema = await buildSchema({
resolvers: [UserResolver, PostResolver],
});
const server = new ApolloServer({
schema,
context: ({ req }) => ({
db: prisma,
user: getUserFromToken(req.headers.authorization),
}),
});
const { url } = await server.listen(4000);
console.log(`Server ready at ${url}`);
}
bootstrap();
Type-Safe Client Queries
Use generated types in your client code:
import { useGetUserQuery, GetUserQuery, GetUserQueryVariables } from './generated/graphql';
function UserProfile({ userId }: { userId: string }) {
// Fully typed query hook
const { data, loading, error } = useGetUserQuery({
variables: { id: userId },
});
// TypeScript knows the exact shape of data
const userName: string | undefined = data?.user?.name;
const userPosts: Array<{ id: string; title: string }> | undefined = data?.user?.posts;
return (
<div>
{userName && <h1>{userName}</h1>}
{userPosts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
End-to-End Type Safety
Achieve complete type safety from database to UI:
// 1. Database schema (Prisma)
model User {
id String @id @default(uuid())
name String
email String @unique
posts Post[]
}
// 2. GraphQL schema (generated or code-first)
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
// 3. TypeScript resolvers (type-checked)
const resolvers: Resolvers<Context> = {
Query: {
user: (_, { id }, { db }) => db.user.findUnique({ where: { id } }),
},
};
// 4. Generated client types
const { data } = useGetUserQuery({ variables: { id: '123' } });
// 5. Type-safe UI rendering
const name: string | undefined = data?.user?.name;
Warning: Keep your codegen configuration synchronized with your schema changes. Always run codegen after modifying GraphQL files.
Exercise:
- Set up GraphQL Code Generator with TypeScript and React Apollo plugins
- Create GraphQL operations (queries and mutations) in separate
.graphqlfiles - Generate TypeScript types and React hooks
- Build a React component using generated hooks with full type safety
- Implement server-side resolvers with generated resolver types