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.