Back to blog

Building Type-Safe GraphQL APIs with TypeScript and Next.js

13 min readBy Mustafa Akkaya
#GraphQL#TypeScript#Next.js#Apollo#API Development

GraphQL revolutionizes API development by giving clients precise control over data fetching. Combined with TypeScript and Next.js, you get end-to-end type safety from database to UI. Let's build a production-ready GraphQL API.

Why GraphQL?

GraphQL solves REST API limitations:

- No Over-fetching - Request exactly what you need

- No Under-fetching - Get related data in one request

- Strong Typing - Schema-first design with built-in validation

- Self-Documenting - Introspection and GraphQL Playground

- Real-time - Built-in subscriptions for live data

- Versionless - Evolve API without breaking changes

Project Setup

Initialize a Next.js project with GraphQL support.

script.sh

# Create Next.js app

npx create-next-app@latest graphql-api --typescript --tailwind --app

cd graphql-api

# Install GraphQL dependencies

npm install graphql graphql-tag @apollo/server @as-integrations/next

npm install @graphql-tools/schema @graphql-codegen/cli

npm install -D @graphql-codegen/typescript @graphql-codegen/typescript-resolvers

npm install -D @graphql-codegen/typescript-operations

# Database & validation

npm install prisma @prisma/client zod

npm install -D prisma

Schema Design

Define your GraphQL schema with strong types.

code.graphql

# graphql/schema.graphql

class="keyword">type User {

id: ID!

email: String!

name: String!

role: UserRole!

posts: [Post!]!

createdAt: DateTime!

updatedAt: DateTime!

}

class="keyword">enum UserRole {

ADMIN

USER

MODERATOR

}

class="keyword">type Post {

id: ID!

title: String!

content: String!

published: Boolean!

author: User!

authorId: String!

tags: [String!]!

views: Int!

createdAt: DateTime!

updatedAt: DateTime!

}

class="keyword">type Query {

# User queries

me: User

user(id: ID!): User

users(skip: Int, take: Int, orderBy: UserOrderBy): UsersResponse!

# Post queries

post(id: ID!): Post

posts(

skip: Int

take: Int

published: Boolean

search: String

): PostsResponse!

}

class="keyword">type Mutation {

# Auth mutations

signup(input: SignupInput!): AuthPayload!

login(input: LoginInput!): AuthPayload!

# User mutations

updateUser(id: ID!, input: UpdateUserInput!): User!

deleteUser(id: ID!): Boolean!

# Post mutations

createPost(input: CreatePostInput!): Post!

updatePost(id: ID!, input: UpdatePostInput!): Post!

deletePost(id: ID!): Boolean!

publishPost(id: ID!): Post!

}

class="keyword">type Subscription {

postCreated: Post!

postUpdated(id: ID!): Post!

}

# Input types

input SignupInput {

email: String!

password: String!

name: String!

}

input LoginInput {

email: String!

password: String!

}

input CreatePostInput {

title: String!

content: String!

tags: [String!]

published: Boolean

}

input UpdatePostInput {

title: String

content: String

tags: [String!]

published: Boolean

}

input UpdateUserInput {

name: String

email: String

role: UserRole

}

class="keyword">enum UserOrderBy {

createdAt_ASC

createdAt_DESC

name_ASC

name_DESC

}

# Response types

class="keyword">type UsersResponse {

users: [User!]!

total: Int!

hasMore: Boolean!

}

class="keyword">type PostsResponse {

posts: [Post!]!

total: Int!

hasMore: Boolean!

}

class="keyword">type AuthPayload {

token: String!

user: User!

}

# Custom scalars

scalar DateTime

Code Generation

Automatically generate TypeScript types from schema.

app.ts

class="keyword">class="comment">// codegen.ts

class="keyword">import class="keyword">type { CodegenConfig } class="keyword">from class="keyword">class="string">"@graphql-codegen/cli";

class="keyword">class="keyword">const config: CodegenConfig = {

schema: class="keyword">class="string">"./graphql/schema.graphql",

generates: {

class="keyword">class="string">"./graphql/generated/types.ts": {

plugins: [class="keyword">class="string">"typescript", class="keyword">class="string">"typescript-resolvers"],

config: {

useIndexSignature: true,

contextType: class="keyword">class="string">"../context#Context",

mappers: {

User: class="keyword">class="string">"@prisma/client#User",

Post: class="keyword">class="string">"@prisma/client#Post",

},

},

},

},

};

class="keyword">export class="keyword">default config;

data.json

class="keyword">class="comment">// package.json scripts

{

class="keyword">class="string">"scripts": {

class="keyword">class="string">"codegen": class="keyword">class="string">"graphql-codegen --config codegen.ts",

class="keyword">class="string">"codegen:watch": class="keyword">class="string">"graphql-codegen --config codegen.ts --watch",

class="keyword">class="string">"prisma:generate": class="keyword">class="string">"prisma generate",

class="keyword">class="string">"dev": class="keyword">class="string">"npm run codegen && next dev"

}

}

Prisma Schema

Set up database models matching GraphQL schema.

code.prisma

class="keyword">class="comment">// prisma/schema.prisma

generator client {

provider = class="keyword">class="string">"prisma-client-js"

}

datasource db {

provider = class="keyword">class="string">"postgresql"

url = env(class="keyword">class="string">"DATABASE_URL")

}

class="keyword">enum UserRole {

ADMIN

USER

MODERATOR

}

model User {

id String @id @class="keyword">default(cuid())

email String @unique

password String

name String

role UserRole @class="keyword">default(USER)

posts Post[]

createdAt DateTime @class="keyword">default(now())

updatedAt DateTime @updatedAt

}

model Post {

id String @id @class="keyword">default(cuid())

title String

content String

published Boolean @class="keyword">default(false)

author User @relation(fields: [authorId], references: [id], onDelete: Cascade)

authorId String

tags String[]

views Int @class="keyword">default(0)

createdAt DateTime @class="keyword">default(now())

updatedAt DateTime @updatedAt

@@index([authorId])

@@index([published])

}

Context Setup

Create resolver context with authentication.

app.ts

class="keyword">class="comment">// graphql/context.ts

class="keyword">import { PrismaClient } class="keyword">from class="keyword">class="string">"@prisma/client";

class="keyword">import { NextRequest } class="keyword">from class="keyword">class="string">"next/server";

class="keyword">import jwt class="keyword">from class="keyword">class="string">"jsonwebtoken";

class="keyword">class="keyword">const prisma = class="keyword">new PrismaClient();

class="keyword">export class="keyword">interface Context {

prisma: PrismaClient;

user: {

id: string;

email: string;

role: string;

} | class="keyword">null;

}

class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function createContext(req: NextRequest): Promise {

class="keyword">class="keyword">const token = req.headers.get(class="keyword">class="string">"authorization")?.replace(class="keyword">class="string">"Bearer ", class="keyword">class="string">"");

class="keyword">class="keyword">let user = class="keyword">null;

class="keyword">class="keyword">if (token) {

class="keyword">try {

class="keyword">class="keyword">const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;

user = {

id: decoded.userId,

email: decoded.email,

role: decoded.role,

};

} class="keyword">catch (error) {

console.error(class="keyword">class="string">"Invalid token:", error);

}

}

class="keyword">class="keyword">return {

prisma,

user,

};

}

Resolvers Implementation

Implement type-safe resolvers.

app.ts

class="keyword">class="comment">// graphql/resolvers/user.ts

class="keyword">import { Resolvers } class="keyword">from class="keyword">class="string">"../generated/types";

class="keyword">import { GraphQLError } class="keyword">from class="keyword">class="string">"graphql";

class="keyword">import bcrypt class="keyword">from class="keyword">class="string">"bcryptjs";

class="keyword">import jwt class="keyword">from class="keyword">class="string">"jsonwebtoken";

class="keyword">export class="keyword">class="keyword">const userResolvers: Resolvers = {

Query: {

me: class="keyword">class="keyword">async (_, __, { user, prisma }) => {

class="keyword">class="keyword">if (!user) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authenticated", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">return class="keyword">class="keyword">await prisma.user.findUnique({

where: { id: user.id },

});

},

user: class="keyword">class="keyword">async (_, { id }, { prisma }) => {

class="keyword">class="keyword">const foundUser = class="keyword">class="keyword">await prisma.user.findUnique({

where: { id },

});

class="keyword">class="keyword">if (!foundUser) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"User not found", {

extensions: { code: class="keyword">class="string">"NOT_FOUND" },

});

}

class="keyword">class="keyword">return foundUser;

},

users: class="keyword">class="keyword">async (_, { skip = 0, take = 10, orderBy }, { prisma }) => {

class="keyword">class="keyword">const [users, total] = class="keyword">class="keyword">await Promise.all([

prisma.user.findMany({

skip,

take,

orderBy: orderBy

? {

[orderBy.split(class="keyword">class="string">"_")[0]]: orderBy.split(class="keyword">class="string">"_")[1].toLowerCase(),

}

: class="keyword">undefined,

}),

prisma.user.count(),

]);

class="keyword">class="keyword">return {

users,

total,

hasMore: skip + take < total,

};

},

},

Mutation: {

signup: class="keyword">class="keyword">async (_, { input }, { prisma }) => {

class="keyword">class="keyword">const existingUser = class="keyword">class="keyword">await prisma.user.findUnique({

where: { email: input.email },

});

class="keyword">class="keyword">if (existingUser) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Email already exists", {

extensions: { code: class="keyword">class="string">"BAD_USER_INPUT" },

});

}

class="keyword">class="keyword">const hashedPassword = class="keyword">class="keyword">await bcrypt.hash(input.password, 10);

class="keyword">class="keyword">const user = class="keyword">class="keyword">await prisma.user.create({

data: {

email: input.email,

name: input.name,

password: hashedPassword,

},

});

class="keyword">class="keyword">const token = jwt.sign(

{ userId: user.id, email: user.email, role: user.role },

process.env.JWT_SECRET!,

{ expiresIn: class="keyword">class="string">"7d" }

);

class="keyword">class="keyword">return { token, user };

},

login: class="keyword">class="keyword">async (_, { input }, { prisma }) => {

class="keyword">class="keyword">const user = class="keyword">class="keyword">await prisma.user.findUnique({

where: { email: input.email },

});

class="keyword">class="keyword">if (!user) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Invalid credentials", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">const valid = class="keyword">class="keyword">await bcrypt.compare(input.password, user.password);

class="keyword">class="keyword">if (!valid) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Invalid credentials", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">const token = jwt.sign(

{ userId: user.id, email: user.email, role: user.role },

process.env.JWT_SECRET!,

{ expiresIn: class="keyword">class="string">"7d" }

);

class="keyword">class="keyword">return { token, user };

},

updateUser: class="keyword">class="keyword">async (_, { id, input }, { user, prisma }) => {

class="keyword">class="keyword">if (!user) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authenticated", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">if (user.id !== id && user.role !== class="keyword">class="string">"ADMIN") {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authorized", {

extensions: { code: class="keyword">class="string">"FORBIDDEN" },

});

}

class="keyword">class="keyword">return class="keyword">class="keyword">await prisma.user.update({

where: { id },

data: input,

});

},

deleteUser: class="keyword">class="keyword">async (_, { id }, { user, prisma }) => {

class="keyword">class="keyword">if (!user || user.role !== class="keyword">class="string">"ADMIN") {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authorized", {

extensions: { code: class="keyword">class="string">"FORBIDDEN" },

});

}

class="keyword">class="keyword">await prisma.user.delete({ where: { id } });

class="keyword">class="keyword">return true;

},

},

User: {

posts: class="keyword">class="keyword">async (parent, _, { prisma }) => {

class="keyword">class="keyword">return class="keyword">class="keyword">await prisma.post.findMany({

where: { authorId: parent.id },

});

},

},

};

class="keyword">class="comment">// graphql/resolvers/post.ts

class="keyword">import { Resolvers } class="keyword">from class="keyword">class="string">"../generated/types";

class="keyword">import { GraphQLError } class="keyword">from class="keyword">class="string">"graphql";

class="keyword">export class="keyword">class="keyword">const postResolvers: Resolvers = {

Query: {

post: class="keyword">class="keyword">async (_, { id }, { prisma }) => {

class="keyword">class="keyword">const post = class="keyword">class="keyword">await prisma.post.findUnique({

where: { id },

include: { author: true },

});

class="keyword">class="keyword">if (!post) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Post not found", {

extensions: { code: class="keyword">class="string">"NOT_FOUND" },

});

}

class="keyword">class="comment">// Increment views

class="keyword">class="keyword">await prisma.post.update({

where: { id },

data: { views: { increment: 1 } },

});

class="keyword">class="keyword">return post;

},

posts: class="keyword">class="keyword">async (

_,

{ skip = 0, take = 10, published, search },

{ prisma }

) => {

class="keyword">class="keyword">const where = {

...(published !== class="keyword">undefined && { published }),

...(search && {

class="keyword">OR: [

{ title: { contains: search, mode: class="keyword">class="string">"insensitive" as class="keyword">class="keyword">const } },

{ content: { contains: search, mode: class="keyword">class="string">"insensitive" as class="keyword">class="keyword">const } },

],

}),

};

class="keyword">class="keyword">const [posts, total] = class="keyword">class="keyword">await Promise.all([

prisma.post.findMany({

where,

skip,

take,

orderBy: { createdAt: class="keyword">class="string">"desc" },

include: { author: true },

}),

prisma.post.count({ where }),

]);

class="keyword">class="keyword">return {

posts,

total,

hasMore: skip + take < total,

};

},

},

Mutation: {

createPost: class="keyword">class="keyword">async (_, { input }, { user, prisma }) => {

class="keyword">class="keyword">if (!user) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authenticated", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">return class="keyword">class="keyword">await prisma.post.create({

data: {

...input,

authorId: user.id,

},

include: { author: true },

});

},

updatePost: class="keyword">class="keyword">async (_, { id, input }, { user, prisma }) => {

class="keyword">class="keyword">if (!user) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authenticated", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">const post = class="keyword">class="keyword">await prisma.post.findUnique({ where: { id } });

class="keyword">class="keyword">if (!post) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Post not found", {

extensions: { code: class="keyword">class="string">"NOT_FOUND" },

});

}

class="keyword">class="keyword">if (post.authorId !== user.id && user.role !== class="keyword">class="string">"ADMIN") {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authorized", {

extensions: { code: class="keyword">class="string">"FORBIDDEN" },

});

}

class="keyword">class="keyword">return class="keyword">class="keyword">await prisma.post.update({

where: { id },

data: input,

include: { author: true },

});

},

deletePost: class="keyword">class="keyword">async (_, { id }, { user, prisma }) => {

class="keyword">class="keyword">if (!user) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authenticated", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">const post = class="keyword">class="keyword">await prisma.post.findUnique({ where: { id } });

class="keyword">class="keyword">if (!post) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Post not found", {

extensions: { code: class="keyword">class="string">"NOT_FOUND" },

});

}

class="keyword">class="keyword">if (post.authorId !== user.id && user.role !== class="keyword">class="string">"ADMIN") {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authorized", {

extensions: { code: class="keyword">class="string">"FORBIDDEN" },

});

}

class="keyword">class="keyword">await prisma.post.delete({ where: { id } });

class="keyword">class="keyword">return true;

},

publishPost: class="keyword">class="keyword">async (_, { id }, { user, prisma }) => {

class="keyword">class="keyword">if (!user) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authenticated", {

extensions: { code: class="keyword">class="string">"UNAUTHENTICATED" },

});

}

class="keyword">class="keyword">const post = class="keyword">class="keyword">await prisma.post.findUnique({ where: { id } });

class="keyword">class="keyword">if (!post) {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Post not found", {

extensions: { code: class="keyword">class="string">"NOT_FOUND" },

});

}

class="keyword">class="keyword">if (post.authorId !== user.id && user.role !== class="keyword">class="string">"ADMIN") {

class="keyword">throw class="keyword">new GraphQLError(class="keyword">class="string">"Not authorized", {

extensions: { code: class="keyword">class="string">"FORBIDDEN" },

});

}

class="keyword">class="keyword">return class="keyword">class="keyword">await prisma.post.update({

where: { id },

data: { published: true },

include: { author: true },

});

},

},

Post: {

author: class="keyword">class="keyword">async (parent, _, { prisma }) => {

class="keyword">class="keyword">return class="keyword">class="keyword">await prisma.user.findUnique({

where: { id: parent.authorId },

});

},

},

};

Apollo Server Integration

Set up Apollo Server in Next.js App Router.

app.ts

class="keyword">class="comment">// app/api/graphql/route.ts

class="keyword">import { ApolloServer } class="keyword">from class="keyword">class="string">"@apollo/server";

class="keyword">import { startServerAndCreateNextHandler } class="keyword">from class="keyword">class="string">"@as-integrations/next";

class="keyword">import { makeExecutableSchema } class="keyword">from class="keyword">class="string">"@graphql-tools/schema";

class="keyword">import { readFileSync } class="keyword">from class="keyword">class="string">"fs";

class="keyword">import { join } class="keyword">from class="keyword">class="string">"path";

class="keyword">import { userResolvers } class="keyword">from class="keyword">class="string">"@/graphql/resolvers/user";

class="keyword">import { postResolvers } class="keyword">from class="keyword">class="string">"@/graphql/resolvers/post";

class="keyword">import { createContext } class="keyword">from class="keyword">class="string">"@/graphql/context";

class="keyword">import { NextRequest } class="keyword">from class="keyword">class="string">"next/server";

class="keyword">class="comment">// Load schema

class="keyword">class="keyword">const typeDefs = readFileSync(

join(process.cwd(), class="keyword">class="string">"graphql/schema.graphql"),

class="keyword">class="string">"utf-8"

);

class="keyword">class="comment">// Merge resolvers

class="keyword">class="keyword">const resolvers = {

Query: {

...userResolvers.Query,

...postResolvers.Query,

},

Mutation: {

...userResolvers.Mutation,

...postResolvers.Mutation,

},

User: userResolvers.User,

Post: postResolvers.Post,

};

class="keyword">class="comment">// Create executable schema

class="keyword">class="keyword">const schema = makeExecutableSchema({

typeDefs,

resolvers,

});

class="keyword">class="comment">// Create Apollo Server

class="keyword">class="keyword">const server = class="keyword">new ApolloServer({

schema,

introspection: process.env.NODE_ENV !== class="keyword">class="string">"production",

});

class="keyword">class="comment">// Create Next.js handler

class="keyword">class="keyword">const handler = startServerAndCreateNextHandler(server, {

context: createContext,

});

class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function GET(request: NextRequest) {

class="keyword">class="keyword">return handler(request);

}

class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function POST(request: NextRequest) {

class="keyword">class="keyword">return handler(request);

}

Client-Side Usage

Use GraphQL in Next.js components.

app.ts

class="keyword">class="comment">// app/posts/page.tsx

class="keyword">class="string">"use client";

class="keyword">import { useEffect, useState } class="keyword">from class="keyword">class="string">"react";

class="keyword">class="keyword">const GET_POSTS =

query GetPosts($skip: Int, $take: Int, $published: Boolean) {

posts(skip: $skip, take: $take, published: $published) {

posts {

id

title

content

views

createdAt

author {

name

}

}

total

hasMore

}

}

;

class="keyword">export class="keyword">default class="keyword">class="keyword">function PostsPage() {

class="keyword">class="keyword">const [posts, setPosts] = useState([]);

class="keyword">class="keyword">const [loading, setLoading] = useState(true);

useEffect(() => {

class="keyword">class="keyword">async class="keyword">class="keyword">function fetchPosts() {

class="keyword">class="keyword">const response = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/graphql", {

method: class="keyword">class="string">"POST",

headers: {

class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json",

},

body: JSON.stringify({

query: GET_POSTS,

variables: {

skip: 0,

take: 10,

published: true,

},

}),

});

class="keyword">class="keyword">const { data } = class="keyword">class="keyword">await response.json();

setPosts(data.posts.posts);

setLoading(false);

}

fetchPosts();

}, []);

class="keyword">class="keyword">if (loading) class="keyword">class="keyword">return

Loading...
;

class="keyword">class="keyword">return (

class="keyword">class="string">"container mx-auto py-8">

class="keyword">class="string">"text-3xl font-bold mb-6">Blog Posts

class="keyword">class="string">"grid gap-6">

{posts.map((post: any) => (

class="keyword">class="string">"border p-6 rounded-lg">

class="keyword">class="string">"text-xl font-bold mb-2">{post.title}

class="keyword">class="string">"text-gray-600 mb-4">

{post.content.slice(0, 200)}...

class="keyword">class="string">"flex justify-between text-sm text-gray-500"> By {post.author.name} {post.views} views

))}

);

}

Apollo Client Setup (Better Approach)

Use Apollo Client for advanced features.

app.ts

class="keyword">class="comment">// lib/apollo-client.ts

class="keyword">import { ApolloClient, InMemoryCache, createHttpLink } class="keyword">from class="keyword">class="string">"@apollo/client";

class="keyword">import { setContext } class="keyword">from class="keyword">class="string">"@apollo/client/link/context";

class="keyword">class="keyword">const httpLink = createHttpLink({

uri: class="keyword">class="string">"/api/graphql",

});

class="keyword">class="keyword">const authLink = setContext((_, { headers }) => {

class="keyword">class="keyword">const token = localStorage.getItem(class="keyword">class="string">"token");

class="keyword">class="keyword">return {

headers: {

...headers,

authorization: token ? class="keyword">class="string">Bearer ${token} : class="keyword">class="string">"",

},

};

});

class="keyword">export class="keyword">class="keyword">const apolloClient = class="keyword">new ApolloClient({

link: authLink.concat(httpLink),

cache: class="keyword">new InMemoryCache(),

});

class="keyword">class="comment">// app/providers.tsx

(class="keyword">class="string">"use client");

class="keyword">import { ApolloProvider } class="keyword">from class="keyword">class="string">"@apollo/client";

class="keyword">import { apolloClient } class="keyword">from class="keyword">class="string">"@/lib/apollo-client";

class="keyword">export class="keyword">class="keyword">function Providers({ children }: { children: React.ReactNode }) {

class="keyword">class="keyword">return {children};

}

class="keyword">class="comment">// Usage in components

(class="keyword">class="string">"use client");

class="keyword">import { useQuery, gql } class="keyword">from class="keyword">class="string">"@apollo/client";

class="keyword">class="keyword">const GET_POSTS = gql

query GetPosts {

posts(published: true) {

posts {

id

title

author {

name

}

}

}

}

;

class="keyword">export class="keyword">class="keyword">function Posts() {

class="keyword">class="keyword">const { data, loading, error } = useQuery(GET_POSTS);

class="keyword">class="keyword">if (loading) class="keyword">class="keyword">return

Loading...

;

class="keyword">class="keyword">if (error) class="keyword">class="keyword">return

Error: {error.message}

;

class="keyword">class="keyword">return (

{data.posts.posts.map((post: any) => (

{post.title}

))}

);

}

Testing GraphQL APIs

Write comprehensive tests.

app.ts

class="keyword">class="comment">// tests/graphql/user.test.ts

class="keyword">import { createTestContext } class="keyword">from class="keyword">class="string">"./__helpers";

describe(class="keyword">class="string">"User Queries", () => {

it(class="keyword">class="string">"should class="keyword">class="keyword">return current user when authenticated", class="keyword">class="keyword">async () => {

class="keyword">class="keyword">const { query, token } = class="keyword">class="keyword">await createTestContext();

class="keyword">class="keyword">const result = class="keyword">class="keyword">await query({

query:

query {

me {

id

email

name

}

}

,

headers: {

authorization: class="keyword">class="string">Bearer ${token},

},

});

expect(result.data.me).toBeDefined();

expect(result.data.me.email).toBe(class="keyword">class="string">"test@example.com");

});

it(class="keyword">class="string">"should class="keyword">throw error when not authenticated", class="keyword">class="keyword">async () => {

class="keyword">class="keyword">const { query } = class="keyword">class="keyword">await createTestContext();

class="keyword">class="keyword">const result = class="keyword">class="keyword">await query({

query:

query {

me {

id

}

}

,

});

expect(result.errors).toBeDefined();

expect(result.errors[0].extensions.code).toBe(class="keyword">class="string">"UNAUTHENTICATED");

});

});

Best Practices

1. Schema-First Design - Design schema before implementation

2. DataLoader - Batch and cache database queries

3. Pagination - Always implement cursor or offset pagination

4. Error Handling - Use GraphQLError with proper codes

5. Rate Limiting - Protect against malicious queries

6. Query Complexity - Limit query depth and complexity

7. N+1 Problem - Use DataLoader to solve

8. Caching - Implement Redis for frequently accessed data

Conclusion

GraphQL with TypeScript and Next.js provides unmatched type safety and developer experience. The combination of code generation, Prisma, and Apollo Server creates a robust, scalable API architecture.

Start with a solid schema design, leverage code generation for type safety, and you'll build APIs that are a joy to work with! 🚀