Building Type-Safe GraphQL APIs with TypeScript and Next.js
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.
# 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.
# 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.
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;
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.
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.
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.
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.
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.
;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.
;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.
,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
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! 🚀