GraphQL with Node.js
GraphQL is a query language for APIs that lets clients request exactly the data they need. Unlike REST’s fixed endpoints, one GraphQL endpoint handles all queries — reducing over-fetching and under-fetching.
GraphQL vs REST
| Aspect | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple URLs | Single /graphql |
| Data fetching | Server decides response shape | Client specifies fields |
| Versioning | /v1/, /v2/ |
Evolve schema, deprecate fields |
| Over-fetching | Common | Eliminated |
| Caching | HTTP caching (simple) | Requires client-side cache (Apollo) |
Use GraphQL when clients need flexible queries (mobile + web with different data needs). Use REST when caching and simplicity matter more.
Setup Apollo Server
mkdir graphql-api && cd graphql-api
npm init -y
npm install @apollo/server graphql
npm install -D typescript @types/node tsx
// src/index.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, body: String!, authorId: ID!): Post!
}
`;
const users = [
{ id: '1', name: 'Alice', email: '[email protected]' },
{ id: '2', name: 'Bob', email: '[email protected]' },
];
const posts = [
{ id: '1', title: 'Hello GraphQL', body: 'First post', authorId: '1' },
{ id: '2', title: 'Node.js Tips', body: 'Second post', authorId: '1' },
];
const resolvers = {
Query: {
users: () => users,
user: (_: unknown, { id }: { id: string }) =>
users.find(u => u.id === id),
posts: () => posts,
},
Mutation: {
createUser: (_: unknown, { name, email }: { name: string; email: string }) => {
const user = { id: String(users.length + 1), name, email };
users.push(user);
return user;
},
createPost: (_: unknown, args: { title: string; body: string; authorId: string }) => {
const post = { id: String(posts.length + 1), ...args };
posts.push(post);
return post;
},
},
User: {
posts: (parent: { id: string }) =>
posts.filter(p => p.authorId === parent.id),
},
Post: {
author: (parent: { authorId: string }) =>
users.find(u => u.id === parent.authorId),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Server ready at ${url}`);
Run: npx tsx src/index.ts
Query Examples
Open http://localhost:4000 for Apollo Sandbox.
Fetch exactly what you need:
query {
users {
name
email
}
}
Nested relationships:
query {
user(id: "1") {
name
posts {
title
body
}
}
}
Mutation:
mutation {
createUser(name: "Carol", email: "[email protected]") {
id
name
}
}
Database Integration with Prisma
npm install @prisma/client
npx prisma init
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const resolvers = {
Query: {
users: () => prisma.user.findMany({ include: { posts: true } }),
user: (_: unknown, { id }: { id: string }) =>
prisma.user.findUnique({ where: { id: parseInt(id) }, include: { posts: true } }),
},
Mutation: {
createUser: (_: unknown, { name, email }: { name: string; email: string }) =>
prisma.user.create({ data: { name, email } }),
},
};
Context and Authentication
interface Context {
userId?: string;
}
const server = new ApolloServer<Context>({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const userId = token ? verifyToken(token) : undefined;
return { userId };
},
});
Protect resolvers:
const resolvers = {
Mutation: {
createPost: (_: unknown, args: CreatePostArgs, { userId }: Context) => {
if (!userId) throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
return prisma.post.create({ data: { ...args, authorId: userId } });
},
},
};
Input Types and Validation
input CreateUserInput {
name: String!
email: String!
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
createUser: (_: unknown, { input }: { input: unknown }) => {
const data = CreateUserSchema.parse(input);
return prisma.user.create({ data });
},
Subscriptions (Real-Time)
npm install graphql-ws ws
type Subscription {
postCreated: Post!
}
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Mutation: {
createPost: async (_: unknown, args: CreatePostArgs) => {
const post = await prisma.post.create({ data: args });
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
},
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
},
},
};
N+1 Problem and DataLoader
Without optimization, fetching 10 users with posts triggers 11 queries:
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (authorIds: readonly string[]) => {
const allPosts = await prisma.post.findMany({
where: { authorId: { in: [...authorIds] } },
});
return authorIds.map(id => allPosts.filter(p => p.authorId === id));
});
const resolvers = {
User: {
posts: (parent: { id: string }) => postLoader.load(parent.id),
},
};
DataLoader batches and caches requests within a single GraphQL operation.
Error Handling
throw new GraphQLError('User not found', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
Production Checklist
- Persisted queries for known client queries
- Query depth and complexity limits
- Rate limiting on
/graphql - Disable introspection in production (or restrict)
- DataLoader for relationship fields
- Authentication on mutations and sensitive queries
GraphQL with Node.js and Apollo Server provides a flexible API layer ideal for mobile apps, dashboards, and multi-client architectures.