Back to Blog

GraphQL API Development: When to Use It and How to Build It Right

GraphQL API development in 2026 — schema design, resolvers, N+1 prevention with DataLoader, authentication, subscriptions, performance, and cost comparison with

Viprasol Tech Team
March 23, 2026
12 min read

GraphQL API Development: When to Use It and How to Build It Right

By Viprasol Tech Team


GraphQL is not a replacement for REST — it's a solution to specific problems that REST handles poorly. Understanding which problems it solves (and which it doesn't) is the prerequisite for making a good architecture decision.

GraphQL solves: over-fetching (REST returns fields you don't need), under-fetching (REST requires multiple requests to get related data), and documentation drift (the schema is always up to date because it's the source of truth). It introduces: query complexity attacks, N+1 database query problems, caching complexity, and a steeper learning curve for clients and developers.

This guide covers when GraphQL is the right choice, how to build a production-grade GraphQL API, and the problems you'll encounter (and must solve) along the way.


When GraphQL Makes Sense

Use GraphQL when:

  • You have a complex data graph where different clients need different subsets of related data (mobile needs less than web, different pages need different combinations)
  • You're building a public API where developers will query diverse combinations of data
  • Your frontend is complex with many components that each need specific data slices
  • Real-time subscriptions are a core feature

Stick with REST when:

  • Your API has straightforward CRUD endpoints
  • File uploads are a primary use case (GraphQL handles these awkwardly)
  • You need HTTP caching at the CDN level (GraphQL POST requests don't cache by default)
  • The client teams are not experienced with GraphQL

Schema Design: The Foundation

Schema-first design — define the GraphQL schema before writing resolvers. The schema is the contract between API and clients:

# schema.graphql

type Query {
  project(id: ID!): Project
  projects(
    first: Int = 20
    after: String
    filter: ProjectFilter
  ): ProjectConnection!
  me: User!
}

type Mutation {
  createProject(input: CreateProjectInput!): CreateProjectPayload!
  updateProject(id: ID!, input: UpdateProjectInput!): UpdateProjectPayload!
  deleteProject(id: ID!): DeleteProjectPayload!
}

type Subscription {
  projectUpdated(id: ID!): Project!
}

type Project {
  id:          ID!
  name:        String!
  description: String
  status:      ProjectStatus!
  owner:       User!
  members:     [User!]!
  tasks(status: TaskStatus): [Task!]!
  createdAt:   DateTime!
  updatedAt:   DateTime!
}

type ProjectConnection {
  edges:    [ProjectEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type ProjectEdge {
  node:   Project!
  cursor: String!
}

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

enum ProjectStatus {
  ACTIVE
  ON_HOLD
  COMPLETED
  ARCHIVED
}

input CreateProjectInput {
  name:        String!
  description: String
  visibility:  Visibility = PRIVATE
}

type CreateProjectPayload {
  project: Project
  errors:  [UserError!]!
}

type UserError {
  field:   String
  message: String!
}

Relay-style connections (ProjectConnection, ProjectEdge, PageInfo) are the standard for paginated lists — they support cursor-based pagination and are expected by frontend GraphQL clients like Relay and Apollo.


🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Resolvers with TypeScript and Pothos

Pothos (formerly GiraphQL) is the best TypeScript-first schema builder — type-safe resolvers without code generation:

import SchemaBuilder from '@pothos/core';
import PrismaPlugin  from '@pothos/plugin-prisma';

const builder = new SchemaBuilder<{
  Context:    { userId: string; db: PrismaClient };
  PrismaTypes: PrismaTypes;
}>({
  plugins: [PrismaPlugin],
  prisma:  { client: db },
});

builder.prismaObject('Project', {
  fields: (t) => ({
    id:          t.exposeID('id'),
    name:        t.exposeString('name'),
    description: t.exposeString('description', { nullable: true }),
    status:      t.exposeString('status'),
    createdAt:   t.expose('createdAt', { type: 'DateTime' }),

    owner: t.relation('owner'),

    // Filtered tasks sub-selection
    tasks: t.prismaField({
      type:     ['Task'],
      nullable: false,
      args: {
        status: t.arg({ type: 'String', required: false }),
      },
      resolve: (query, project, { status }) =>
        db.task.findMany({
          ...query,
          where: {
            projectId: project.id,
            ...(status ? { status } : {}),
          },
          orderBy: { createdAt: 'desc' },
        }),
    }),
  }),
});

builder.queryField('project', (t) =>
  t.prismaField({
    type:     'Project',
    nullable: true,
    args: {
      id: t.arg.id({ required: true }),
    },
    resolve: async (query, _root, { id }, ctx) => {
      const project = await db.project.findUnique({
        ...query,
        where: { id: String(id) },
      });
      // Authorization: user must be owner or member
      if (!project) return null;
      const isMember = await db.projectMember.findUnique({
        where: { projectId_userId: { projectId: project.id, userId: ctx.userId } },
      });
      if (project.ownerId !== ctx.userId && !isMember) return null;
      return project;
    },
  })
);

The N+1 Problem and DataLoader

The most common GraphQL performance problem: requesting 100 projects, each with an owner field — causes 100 separate database queries for the owners (1 for projects + 100 for owners = 101 queries).

DataLoader solves this by batching and caching database requests within a single GraphQL request:

import DataLoader from 'dataloader';

// Create loaders per request (not global — avoids cross-request data leakage)
function createLoaders(db: PrismaClient) {
  return {
    userById: new DataLoader<string, User | null>(async (ids) => {
      const users = await db.user.findMany({
        where: { id: { in: [...ids] } },
      });
      const userMap = new Map(users.map(u => [u.id, u]));
      // Must return in same order as input ids
      return ids.map(id => userMap.get(id) ?? null);
    }),

    projectMembersByProjectId: new DataLoader<string, User[]>(async (projectIds) => {
      const memberships = await db.projectMember.findMany({
        where:   { projectId: { in: [...projectIds] } },
        include: { user: true },
      });
      const memberMap = new Map<string, User[]>();
      for (const m of memberships) {
        const existing = memberMap.get(m.projectId) ?? [];
        memberMap.set(m.projectId, [...existing, m.user]);
      }
      return projectIds.map(id => memberMap.get(id) ?? []);
    }),
  };
}

// In the resolver — DataLoader batches all owner requests into one query
builder.prismaObject('Project', {
  fields: (t) => ({
    owner: t.field({
      type:    'User',
      resolve: (project, _args, ctx) =>
        ctx.loaders.userById.load(project.ownerId),
        // DataLoader batches 100 calls into: SELECT * FROM users WHERE id IN (...)
    }),
  }),
});

100 projects → 2 queries total (projects + users), not 101. DataLoader is not optional in production GraphQL — it's the difference between 100ms and 10 second response times.


🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Authentication and Authorization in GraphQL

GraphQL has a single endpoint (/graphql) unlike REST's many endpoints. Authentication is handled at the HTTP level (JWT verification before the GraphQL execution begins); authorization is handled in resolvers:

// Context builder — runs before every request
async function buildContext({ req }: { req: Request }) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  let userId: string | null = null;
  if (token) {
    try {
      const payload = await verifyAccessToken(token);
      userId = payload.sub;
    } catch {
      // Invalid token — context has null userId
    }
  }

  return {
    userId,
    db,
    loaders: createLoaders(db),
  };
}

// Pothos auth plugin for field-level authorization
builder.queryField('adminStats', (t) =>
  t.field({
    type:    AdminStats,
    authScopes: { isAdmin: true },  // only accessible to admin users
    resolve: () => getAdminStats(),
  })
);

Subscriptions (Real-Time Updates)

GraphQL subscriptions use WebSocket for server-to-client push:

import { createServer }      from 'http';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer }   from 'ws';
import { useServer }         from 'graphql-ws/lib/use/ws';

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

useServer({
  schema,
  context: async (ctx) => {
    const token = ctx.connectionParams?.authorization as string | undefined;
    const userId = token ? await verifyTokenOrNull(token) : null;
    return { userId, db, loaders: createLoaders(db) };
  },
}, wsServer);

// Resolver using PubSub
const resolvers = {
  Subscription: {
    projectUpdated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator(['PROJECT_UPDATED']),
        (payload, variables, context) =>
          payload.projectUpdated.id === variables.id &&
          canUserAccessProject(context.userId, payload.projectUpdated.id),
      ),
    },
  },
};

Query Complexity and Depth Limiting

GraphQL's flexible queries can be weaponized — a deeply nested query can generate thousands of database calls:

# Malicious deep query
{ project { tasks { assignee { projects { tasks { assignee { ... } } } } } } }

Protect with graphql-depth-limit and query complexity analysis:

import depthLimit from 'graphql-depth-limit';
import { createComplexityRule } from 'graphql-query-complexity';

const server = new ApolloServer({
  schema,
  validationRules: [
    depthLimit(7),  // max 7 levels of nesting
    createComplexityRule({
      maximumComplexity: 1000,   // each field has a cost; total must be < 1000
      variables:         {},
      onComplete: (complexity) => {
        if (complexity > 500) console.warn(`High query complexity: ${complexity}`);
      },
    }),
  ],
});

Cost Ranges for GraphQL API Development

Project TypeScopeCost RangeTimeline
GraphQL API (replacing REST)20–40 types + resolvers$30K–$80K8–14 weeks
GraphQL with subscriptionsReal-time + WebSocket$50K–$120K3–5 months
Public GraphQL API + federationMulti-service schema federation$80K–$200K4–8 months

Working With Viprasol

Our API development practice includes GraphQL schema design, Pothos type-safe resolvers, DataLoader optimization, subscriptions, and query complexity protection. We use TypeScript throughout and code-generate types from the schema for complete end-to-end type safety.

Building a GraphQL API? Viprasol Tech designs and implements production GraphQL APIs. Contact us.


See also: API Development Company · Node.js Development Company · React Development Company

Sources: GraphQL Specification · Pothos Documentation · DataLoader Documentation

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.