Developing GraphQL API for Web Application
GraphQL is a query language for APIs and a runtime for executing them. The client requests exactly the fields it needs and gets precisely those—no more, no less. This solves two REST problems: over-fetching (extra data) and under-fetching (multiple requests for one screen).
Core Concepts
Schema-first: API is defined through types:
type Article {
id: ID!
title: String!
body: String!
author: User!
tags: [Tag!]!
createdAt: DateTime!
}
type Query {
article(id: ID!): Article
articles(filter: ArticleFilter, page: Int, limit: Int): ArticleConnection!
}
type Mutation {
createArticle(input: CreateArticleInput!): Article!
updateArticle(id: ID!, input: UpdateArticleInput!): Article!
}
type Subscription {
articleUpdated(id: ID!): Article!
}
Queries and Fragments
# Client requests only needed fields
query ArticlePage($id: ID!) {
article(id: $id) {
title
body
author {
name
avatar
}
tags { name, slug }
}
}
# Reusable fragments
fragment ArticleCard on Article {
id, title, slug
author { name }
createdAt
}
query ArticleList {
articles(limit: 10) {
nodes { ...ArticleCard }
pageInfo { hasNextPage, endCursor }
}
}
N+1 Problem and DataLoader
GraphQL's main technical issue—N+1 queries. For a list of 20 articles with author field: 1 + 20 = 21 SQL queries.
Solution—DataLoader (Facebook, available for all languages):
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...userIds] } }
});
return userIds.map(id => users.find(u => u.id === id));
});
// In resolver
const articleResolver = {
author: (article, _, { loaders }) => loaders.user.load(article.authorId),
};
// Now: 1 query for articles + 1 batch query for all authors
Implementation (Node.js + Apollo Server + Prisma)
import { ApolloServer } from '@apollo/server';
import { makeExecutableSchema } from '@graphql-tools/schema';
const typeDefs = gql`...`;
const resolvers = {
Query: {
article: async (_, { id }, { db }) =>
db.article.findUnique({ where: { id } }),
articles: async (_, { filter, page = 1, limit = 20 }, { db }) =>
db.article.findMany({
where: filter ? { status: filter.status } : undefined,
skip: (page - 1) * limit,
take: limit,
}),
},
Mutation: {
createArticle: async (_, { input }, { db, user }) => {
if (!user) throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' }
});
return db.article.create({ data: { ...input, authorId: user.id } });
},
},
};
const server = new ApolloServer({ schema: makeExecutableSchema({ typeDefs, resolvers }) });
Authorization at Resolver Level
import { shield, rule, and } from 'graphql-shield';
const isAuthenticated = rule()((_, __, ctx) => !!ctx.user);
const isOwner = rule()(async (_, { id }, ctx) => {
const article = await ctx.db.article.findUnique({ where: { id } });
return article.authorId === ctx.user?.id;
});
const permissions = shield({
Mutation: {
createArticle: isAuthenticated,
updateArticle: and(isAuthenticated, isOwner),
},
});
Subscriptions
subscription CommentAdded($articleId: ID!) {
commentAdded(articleId: $articleId) {
id, body, author { name }
}
}
Implementation via WebSocket (graphql-ws) + Redis Pub/Sub for scaling across instances.







