JavaScript 10 min read

GraphQL API Development: Complete Guide with Node.js

Master GraphQL API development from scratch. Learn schema design, resolvers, queries, mutations, subscriptions, and authentication best practices.

MR

Moshiour Rahman

· Updated May 9, 2026

What is GraphQL?

GraphQL is a query language for APIs that allows clients to request exactly the data they need. Unlike REST, it provides a single endpoint and lets clients specify their data requirements.

GraphQL vs REST

RESTGraphQL
Multiple endpointsSingle endpoint
Fixed response shapeFlexible queries
Over/under fetchingExact data needed
Multiple requestsSingle request

Getting Started

Setup with Apollo Server

npm init -y
npm install @apollo/server graphql
npm install -D typescript @types/node ts-node

Basic Server

// src/index.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello, GraphQL!'
  }
};

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

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 }
});

console.log(`Server running at ${url}`);

Schema Definition

Types

# Scalar types
type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  balance: Float
  isActive: Boolean!
  createdAt: String
}

# Object types
type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

# Enum types
enum Role {
  USER
  ADMIN
  MODERATOR
}

# Input types
input CreateUserInput {
  name: String!
  email: String!
  password: String!
  role: Role = USER
}

input UpdateUserInput {
  name: String
  email: String
}

Queries and Mutations

type Query {
  # Get single item
  user(id: ID!): User
  post(id: ID!): Post

  # Get lists
  users(limit: Int, offset: Int): [User!]!
  posts(published: Boolean): [Post!]!

  # Search
  searchUsers(query: String!): [User!]!
}

type Mutation {
  # Create
  createUser(input: CreateUserInput!): User!
  createPost(title: String!, content: String!, authorId: ID!): Post!

  # Update
  updateUser(id: ID!, input: UpdateUserInput!): User
  publishPost(id: ID!): Post

  # Delete
  deleteUser(id: ID!): Boolean!
  deletePost(id: ID!): Boolean!
}

type Subscription {
  postCreated: Post!
  userOnline(userId: ID!): User!
}

Resolvers

Basic Resolvers

// src/resolvers.ts
const users = [
  { id: '1', name: 'John', email: 'john@example.com', isActive: true },
  { id: '2', name: 'Jane', email: 'jane@example.com', isActive: true }
];

const posts = [
  { id: '1', title: 'GraphQL Guide', content: '...', published: true, authorId: '1' },
  { id: '2', title: 'Node.js Tips', content: '...', published: false, authorId: '2' }
];

const resolvers = {
  Query: {
    user: (_, { id }) => users.find(u => u.id === id),
    users: (_, { limit = 10, offset = 0 }) =>
      users.slice(offset, offset + limit),
    post: (_, { id }) => posts.find(p => p.id === id),
    posts: (_, { published }) =>
      published !== undefined
        ? posts.filter(p => p.published === published)
        : posts
  },

  Mutation: {
    createUser: (_, { input }) => {
      const user = { id: String(users.length + 1), ...input, isActive: true };
      users.push(user);
      return user;
    },
    updateUser: (_, { id, input }) => {
      const index = users.findIndex(u => u.id === id);
      if (index === -1) return null;
      users[index] = { ...users[index], ...input };
      return users[index];
    },
    deleteUser: (_, { id }) => {
      const index = users.findIndex(u => u.id === id);
      if (index === -1) return false;
      users.splice(index, 1);
      return true;
    }
  },

  // Field resolvers
  Post: {
    author: (post) => users.find(u => u.id === post.authorId)
  },

  User: {
    posts: (user) => posts.filter(p => p.authorId === user.id)
  }
};

Context and Authentication

// src/context.ts
import { verifyToken } from './auth';

export interface Context {
  user: User | null;
  dataSources: DataSources;
}

export async function createContext({ req }): Promise<Context> {
  const token = req.headers.authorization?.replace('Bearer ', '');

  let user = null;
  if (token) {
    try {
      user = await verifyToken(token);
    } catch (error) {
      // Invalid token
    }
  }

  return {
    user,
    dataSources: {
      users: new UserDataSource(),
      posts: new PostDataSource()
    }
  };
}

// Usage in resolvers
const resolvers = {
  Query: {
    me: (_, __, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      return context.user;
    }
  },

  Mutation: {
    createPost: (_, { input }, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      return context.dataSources.posts.create({
        ...input,
        authorId: context.user.id
      });
    }
  }
};

Database Integration

With Prisma

// src/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

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

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}
// src/resolvers.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

const resolvers = {
  Query: {
    users: () => prisma.user.findMany(),
    user: (_, { id }) => prisma.user.findUnique({ where: { id } }),
    posts: (_, { published }) =>
      prisma.post.findMany({
        where: published !== undefined ? { published } : undefined,
        include: { author: true }
      })
  },

  Mutation: {
    createUser: (_, { input }) =>
      prisma.user.create({ data: input }),

    createPost: (_, { title, content, authorId }) =>
      prisma.post.create({
        data: { title, content, authorId },
        include: { author: true }
      }),

    publishPost: (_, { id }) =>
      prisma.post.update({
        where: { id },
        data: { published: true }
      })
  },

  User: {
    posts: (user) => prisma.post.findMany({ where: { authorId: user.id } })
  }
};

Advanced Features

Pagination

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type PostEdge {
  cursor: String!
  node: Post!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type Query {
  posts(first: Int, after: String, last: Int, before: String): PostConnection!
}
const resolvers = {
  Query: {
    posts: async (_, { first = 10, after }) => {
      const cursor = after ? { id: after } : undefined;

      const posts = await prisma.post.findMany({
        take: first + 1,
        skip: cursor ? 1 : 0,
        cursor,
        orderBy: { createdAt: 'desc' }
      });

      const hasNextPage = posts.length > first;
      const edges = posts.slice(0, first).map(post => ({
        cursor: post.id,
        node: post
      }));

      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor
        },
        totalCount: await prisma.post.count()
      };
    }
  }
};

DataLoader (N+1 Problem)

import DataLoader from 'dataloader';

// Create loaders
const createLoaders = () => ({
  userLoader: new DataLoader(async (ids: string[]) => {
    const users = await prisma.user.findMany({
      where: { id: { in: ids } }
    });
    const userMap = new Map(users.map(u => [u.id, u]));
    return ids.map(id => userMap.get(id));
  }),

  postsByAuthorLoader: new DataLoader(async (authorIds: string[]) => {
    const posts = await prisma.post.findMany({
      where: { authorId: { in: authorIds } }
    });
    const postMap = new Map<string, Post[]>();
    posts.forEach(post => {
      const existing = postMap.get(post.authorId) || [];
      postMap.set(post.authorId, [...existing, post]);
    });
    return authorIds.map(id => postMap.get(id) || []);
  })
});

// Use in resolvers
const resolvers = {
  Post: {
    author: (post, _, { loaders }) =>
      loaders.userLoader.load(post.authorId)
  },
  User: {
    posts: (user, _, { loaders }) =>
      loaders.postsByAuthorLoader.load(user.id)
  }
};

Subscriptions

import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const typeDefs = `#graphql
  type Subscription {
    postCreated: Post!
    commentAdded(postId: ID!): Comment!
  }
`;

const resolvers = {
  Mutation: {
    createPost: async (_, { input }, context) => {
      const post = await prisma.post.create({ data: input });
      pubsub.publish('POST_CREATED', { postCreated: post });
      return post;
    },

    addComment: async (_, { postId, text }, context) => {
      const comment = await prisma.comment.create({
        data: { postId, text, authorId: context.user.id }
      });
      pubsub.publish(`COMMENT_ADDED_${postId}`, { commentAdded: comment });
      return comment;
    }
  },

  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
    },
    commentAdded: {
      subscribe: (_, { postId }) =>
        pubsub.asyncIterator([`COMMENT_ADDED_${postId}`])
    }
  }
};

// Setup WebSocket server
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql'
});

useServer({ schema }, wsServer);

Error Handling

import { GraphQLError } from 'graphql';

const resolvers = {
  Mutation: {
    createUser: async (_, { input }) => {
      const existing = await prisma.user.findUnique({
        where: { email: input.email }
      });

      if (existing) {
        throw new GraphQLError('Email already exists', {
          extensions: {
            code: 'BAD_USER_INPUT',
            field: 'email'
          }
        });
      }

      return prisma.user.create({ data: input });
    }
  }
};

// Custom error formatting
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (error) => {
    // Log error
    console.error(error);

    // Hide internal errors in production
    if (process.env.NODE_ENV === 'production') {
      if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
        return new GraphQLError('Internal server error');
      }
    }

    return error;
  }
});

Directives

directive @auth(requires: Role = USER) on FIELD_DEFINITION
directive @deprecated(reason: String) on FIELD_DEFINITION

type Query {
  publicPosts: [Post!]!
  myPosts: [Post!]! @auth
  adminData: AdminData! @auth(requires: ADMIN)
  oldField: String @deprecated(reason: "Use newField instead")
}
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';

function authDirectiveTransformer(schema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];

      if (authDirective) {
        const { resolve = defaultFieldResolver } = fieldConfig;
        const { requires } = authDirective;

        fieldConfig.resolve = async (source, args, context, info) => {
          if (!context.user) {
            throw new GraphQLError('Not authenticated');
          }

          if (requires && context.user.role !== requires) {
            throw new GraphQLError('Not authorized');
          }

          return resolve(source, args, context, info);
        };
      }

      return fieldConfig;
    }
  });
}

Client Usage

Query Examples

# Get user with posts
query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    posts {
      id
      title
      published
    }
  }
}

# Search with variables
query SearchPosts($query: String!, $limit: Int) {
  searchPosts(query: $query, limit: $limit) {
    id
    title
    author {
      name
    }
  }
}

# Fragments
fragment UserFields on User {
  id
  name
  email
}

query GetUsers {
  users {
    ...UserFields
    posts {
      title
    }
  }
}

React Client

import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache()
});

// Query hook
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Mutation hook
const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading }] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }]
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    createUser({
      variables: {
        input: { name: 'New User', email: 'new@example.com' }
      }
    });
  };

  return <button onClick={handleSubmit} disabled={loading}>Create User</button>;
}

Summary

ConceptPurpose
SchemaDefine types and operations
ResolversImplement data fetching
ContextShare auth and data sources
DataLoaderSolve N+1 problem
SubscriptionsReal-time updates
DirectivesDeclarative field behavior

GraphQL provides flexible, efficient API development with strong typing and excellent tooling.

Is your AI API down right now? Check live status for OpenAI, Claude, Gemini, Groq, and more.
Check status →
MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.