Skip to content
Learni
View all tutorials
Backend

How to Implement Advanced GraphQL with Apollo in 2026

Lire en français

Introduction

GraphQL is the gold standard for modern APIs in 2026, outshining REST with superior flexibility and efficiency. This advanced tutorial walks you through building a full-featured API using Apollo Server 4, Prisma 6 as the ORM, JWT authentication, request batching, caching, and WebSocket subscriptions for real-time features. Picture a blog app with users, posts, and comments: clients get precisely the data they need, eliminating over-fetching.

Why does it matter? GraphQL APIs cut production latency by 40-60%, tackle N+1 problems with DataLoader, and scale via federation. We'll create a production-ready TypeScript server that's testable and secure. By the end, you'll deploy it to Vercel or a Kubernetes cluster. Ready to supercharge your backends? (128 words)

Prerequisites

  • Node.js 20+ and npm 10+
  • Advanced TypeScript and async/await knowledge
  • Familiarity with Prisma and relational databases
  • Tools: GraphQL Playground or Apollo Studio for testing

Project Initialization

terminal
mkdir graphql-advanced-api && cd graphql-advanced-api
npm init -y
npm install @apollo/server @apollo/server-plugin-drain-http-server graphql prisma @prisma/client jsonwebtoken bcryptjs @types/jsonwebtoken @types/bcryptjs @types/node
databases/sqlite
docker run --name prisma-db -e POSTGRES_PASSWORD=prisma -p 5432:5432 -d postgres
npm install -D typescript ts-node @types/express express cors
npx prisma init --datasource-provider sqlite

This script sets up a Node.js project with Apollo Server, Prisma (SQLite for simplicity and portability), JWT for authentication, and TypeScript dependencies. Optional Postgres container; using SQLite here for standalone runs. Skip cloud databases in development for better reproducibility.

Prisma Setup

Prisma generates a type-safe client based on our schema: Users, Posts, Comments with full relations. We define advanced models with indexes and unique constraints for optimal scaling.

Complete Prisma Schema

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model User {
  id       String   @id @default(uuid())
  email    String   @unique
  password String
  name     String?
  posts    Post[]
  createdAt DateTime @default(now())
}

model Post {
  id          String     @id @default(uuid())
  title       String
  content     String
  authorId    String
  author      User       @relation(fields: [authorId], references: [id])
  comments    Comment[]
  createdAt   DateTime   @default(now())
  @@index([authorId])
}

model Comment {
  id        String   @id @default(uuid())
  content   String
  postId    String
  post      Post     @relation(fields: [postId], references: [id])
  createdAt DateTime @default(now())
  @@index([postId])
}

Full relational schema with UUIDs, performance indexes, and timestamps. SQLite enables fast local development; switch to Postgres for production. Run npx prisma migrate dev --name init followed by npx prisma generate.

Advanced GraphQL TypeDefs

src/schema.ts
import { gql } from 'graphql-tag';

export const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    name: String
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
  }

  type Comment {
    id: ID!
    content: String!
    post: Post!
  }

  type Query {
    me: User
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createPost(title: String!, content: String!): Post!
    deletePost(id: ID!): Boolean!
    addComment(postId: ID!, content: String!): Comment!
  }

  type Subscription {
    postAdded: Post!
  }

  scalar DateTime
`; 

Strict type definitions with relations and subscriptions. Resolvers prevent N+1 issues. Include a DateTime scalar to handle Prisma timestamps.

Resolvers with DataLoader and Auth

Resolvers manage core business logic: N+1-proof batching via DataLoader, JWT authentication in context, and bcrypt password hashing.

JWT and Bcrypt Utilities

src/utils/auth.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';

const JWT_SECRET = process.env.JWT_SECRET || 'supersecretdevkey2026';

export const hashPassword = async (password: string): Promise<string> => {
  return bcrypt.hash(password, 12);
};

export const verifyPassword = async (password: string, hash: string): Promise<boolean> => {
  return bcrypt.compare(password, hash);
};

export const createToken = (payload: { userId: string }): string => {
  return jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
};

export const verifyToken = (token: string): { userId: string } | null => {
  try {
    return jwt.verify(token, JWT_SECRET) as { userId: string };
  } catch {
    return null;
  }
};

Secure utilities for JWT (short expiration) and bcrypt (salt rounds 12). Use a JWT_SECRET environment variable in production with dotenv. Pitfall: Never commit secrets to version control.

Context with Auth and DataLoader

src/context.ts
import { Request } from 'express';
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
import { verifyToken } from './utils/auth';

const prisma = new PrismaClient();

export interface Context {
  prisma: PrismaClient;
  userId: string | null;
  loaders: {
    users: DataLoader<string, any>;
    postsByUser: DataLoader<string, any[]>;
  };
}

export const createContext = ({ req }: { req: Request }): Context => {
  const token = req.headers.authorization?.replace('Bearer ', '') || '';
  const userId = verifyToken(token)?.userId || null;
  return {
    prisma,
    userId,
    loaders: {
      users: new DataLoader(async (ids) => {/* implement batch */}),
      postsByUser: new DataLoader(async (userIds) => {/* batch posts by user */}),
    },
  };
};

Advanced context provides Prisma client, authenticated user ID, and DataLoaders for efficient batching. Flesh out batch functions in resolvers. Tip: Call prisma.$disconnect() on server shutdown to avoid memory leaks.

Complete Resolvers with Batching

src/resolvers.ts
import { Resolvers } from './types'; // généré par codegen
import { Context } from './context';

export const resolvers: Resolvers<Context> = {
  Query: {
    me: async (_, __, { userId, prisma }) => {
      if (!userId) throw new Error('Non authentifié');
      return prisma.user.findUnique({ where: { id: userId }, include: { posts: true } });
    },
    posts: async (_, __, { prisma }) => {
      return prisma.post.findMany({ include: { author: true, comments: true } });
    },
    post: async (_, { id }, { prisma }) => {
      return prisma.post.findUnique({ where: { id }, include: { author: true, comments: true } });
    },
  },
  Post: {
    author: async (parent, _, { loaders }) => {
      return loaders.users.load(parent.authorId);
    },
  },
  Mutation: {
    createPost: async (_, { title, content }, { userId, prisma }) => {
      if (!userId) throw new Error('Auth required');
      return prisma.post.create({ data: { title, content, authorId: userId }, include: { author: true } });
    },
  },
  Subscription: {
    postAdded: {
      subscribe: () => {/* PubSub implementation */},
    },
  },
};

Type-safe resolvers with authentication guards, Prisma relation includes, and DataLoader integration. Add PubSub for subscriptions. N+1 issues eliminated through batched loaders.

Apollo Server with Plugins

src/server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
import cors from 'cors';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
import { createContext } from './context';

const app = express();
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

await server.start();

app.use('/graphql', cors<cors.CorsRequest>(), express.json(), expressMiddleware(server, { context: createContext }));

const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`🚀 Server ready at ${url}`);

Hybrid Express/standalone server with context middleware. Built-in CORS and JSON handling. Default caching enabled. Launch via ts-node src/server.ts and test at http://localhost:4000/graphql.

Testing and Subscriptions

Example Query: { posts { title author { name } } }. Mutation: mutation { createPost(title: "Test", content: "Content") { id } } with Authorization: Bearer header. Subscriptions over WS: subscription { postAdded { title } }.

Apollo Client for Testing

test-client.js
const { ApolloClient, InMemoryCache, gql, split, HttpLink, WebSocketLink } = require('@apollo/client');
const { getMainDefinition } = require('@apollo/client/utilities');

const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
const wsLink = new WebSocketLink({ uri: 'ws://localhost:4000/graphql', options: { reconnect: true } });

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });

client.query({ query: gql`query { posts { title } }` }).then(console.log);

Split Apollo Client for HTTP queries/mutations and WS subscriptions. In-memory caching. Install @apollo/client and run to verify setup. Pitfall: WebSocket URI must omit 'http' or 'https'.

Best Practices

  • Batching essential: Use DataLoader for all 1:N relations.
  • Field-level auth: Implement guards in resolvers, not globally.
  • Advanced caching: Apollo's built-in cache plus Redis in production.
  • Rate limiting: Apollo Server plugins or Express middleware.
  • Schema stitching: Ideal for microservices federation.
  • Monitoring: Apollo Studio or Prometheus metrics.

Common Pitfalls to Avoid

  • N+1 queries: Without DataLoader, 100 posts trigger 100+ database calls.
  • Unverified JWTs: Invalid tokens crash without try/catch.
  • Subscriptions without PubSub: Import PubSub from 'graphql-subscriptions'.
  • Prisma leaks: Always call prisma.$disconnect() on shutdown.
  • Missing scalars: Define custom resolvers for DateTime.

Next Steps

Dive deeper into Apollo Federation for microservices. Scale with Postgres and Kafka. Explore our advanced GraphQL trainings at Learni and the Prisma + Apollo workshop.