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

Advertisement

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.

Advertisement

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.