GraphQL

GraphQL with TypeScript

20 min Lesson 30 of 35

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:
  1. Set up GraphQL Code Generator with TypeScript and React Apollo plugins
  2. Create GraphQL operations (queries and mutations) in separate .graphql files
  3. Generate TypeScript types and React hooks
  4. Build a React component using generated hooks with full type safety
  5. Implement server-side resolvers with generated resolver types