Skip to content

Graphql Api Design and Best Practices — Complete Guide

DodaTech Updated 2026-06-23 9 min read

In this tutorial, you'll learn about GraphQL Api Design and Best Practices. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

GraphQL is a query language and runtime for APIs that lets clients request exactly the data they need, reducing overfetching and underfetching while providing a strongly typed schema for self-documenting APIs.

What You'll Learn

You will learn GraphQL schema design, query and mutation patterns, resolver implementation, batching with DataLoader, subscription setup, and production security practices for GraphQL APIs.

Why GraphQL Matters

GraphQL solves three major problems of REST APIs: overfetching (getting too much data), underfetching (needing multiple requests), and versioning (evolving the schema without breaking clients). Companies like GitHub, Shopify, and Meta use GraphQL in production, serving billions of requests daily.

Real-World Use

DodaTech uses GraphQL for the Doda Browser dashboard where different views need different user data, DodaZIP uses GraphQL for its analytics dashboard with customizable data grids, and Durga Antivirus Pro uses GraphQL for threat intelligence queries where analysts need flexible data combinations.

GraphQL Api Design Learning Path

flowchart LR
  A[REST API Basics] --> B[GraphQL Concepts]
  B --> C[Schema & Types]
  C --> D[Queries & Mutations]
  D --> E[Resolvers]
  E --> F[DataLoader]
  F --> G[Subscriptions & Security]
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

Prerequisites

Understand RESTful Api Design Best Practices and API Development Concepts. Familiarity with JSON Data Format and a programming language like JavaScript Basics or Python Basics is required.

What is GraphQL?

GraphQL was developed by Facebook in 2012 and open-sourced in 2015. It is not a database technology. It is a middle layer between clients and data sources.

Key concepts:

  • Schema — Defines what data is available and its types
  • Query — Read data (equivalent to REST GET)
  • Mutation — Write data (equivalent to REST POST, PUT, PATCH, DELETE)
  • Subscription — Real-time data (equivalent to WebSocket)
  • Resolver — Function that fetches data for a field
  • Type — Defines the shape of an object

Schema Design

The Schema Definition Language (SDL)

# Define types
type User {
  id: ID!
  name: String!
  email: String!
  role: Role!
  posts: [Post!]!
  createdAt: String!
}

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

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

enum Role {
  ADMIN
  MEMBER
  VIEWER
}

# Entry points
type Query {
  users(page: Int, limit: Int): [User!]!
  user(id: ID!): User
  posts(authorId: ID): [Post!]!
  post(id: ID!): Post
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
}

type Subscription {
  postCreated: Post!
  commentAdded(postId: ID!): Comment!
}

input CreateUserInput {
  name: String!
  email: String!
  password: String!
  role: Role!
}

input UpdateUserInput {
  name: String
  email: String
  role: Role
}

Design Principles

  1. Prefer nullable fields — Not every field needs to be required. Use ! only when the value is guaranteed.

  2. Use custom types over scalars — Instead of email: String, create an Email scalar with validation.

  3. Design for client needs — The schema should reflect how clients consume data, not how it is stored in the database.

  4. Limit depth — Allow reasonable nesting but prevent circular queries that cause performance issues.

Implementing a GraphQL Server with Apollo Server

Step 1: Setup

npm init -y
npm install @apollo/server graphql

Step 2: Define Schema

const { ApolloServer } = require("@apollo/server");
const { startStandaloneServer } = require("@apollo/server/standalone");

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

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
`;

Step 3: Implement Resolvers

const users = [
  { id: "1", name: "Alice", email: "alice"@example".com" },
  { id: "2", name: "Bob", email: "bob"@example".com" }
];

const posts = [
  { id: "1", title: "GraphQL Guide", content: "Content...", authorId: "1" },
  { id: "2", title: "REST vs GraphQL", content: "Content...", authorId: "1" }
];

const resolvers = {
  Query: {
    users: () => users,
    user: (_, { id }) => users.find(u => u.id === id),
    posts: () => posts
  },
  Mutation: {
    createUser: (_, { name, email }) => {
      const user = { id: String(users.length + 1), name, email };
      users.push(user);
      return user;
    },
    createPost: (_, { title, content, authorId }) => {
      const post = { id: String(posts.length + 1), title, content, authorId };
      posts.push(post);
      return post;
    }
  },
  User: {
    posts: (parent) => posts.filter(p => p.authorId === parent.id)
  },
  Post: {
    author: (parent) => users.find(u => u.id === parent.authorId)
  }
};

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

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

Expected output:

Server ready at http://localhost:4000/

Step 4: Execute Queries

Open Apollo Sandbox at HTTP://localhost:4000 and run:

query GetUsers {
  users {
    id
    name
    email
    posts {
      title
    }
  }
}

Expected result:

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "Alice",
        "email": "alice"@example".com",
        "posts": [
          { "title": "Graphql Guide" },
          { "title": "REST vs Graphql" }
        ]
      },
      {
        "id": "2",
        "name": "Bob",
        "email": "bob@example.com",
        "posts": []
      }
    ]
  }
}

Solving the N+1 Problem with DataLoader

The N+1 Problem occurs when GraphQL makes one query for the parent and N queries for each child.

// Without DataLoader: N+1 Problem
// Getting 100 users and their posts makes 1 + 100 = 101 queries
User: {
  posts: (parent) => db.query("SELECT * FROM posts WHERE author_id = $1", [parent.id])
}

Solution: DataLoader

const DataLoader = require("dataloader");

const batchPostsByAuthorIds = async (authorIds) => {
  const posts = await db.query(
    "SELECT * FROM posts WHERE author_id = ANY($1)",
    [authorIds]
  );
  return authorIds.map(id => posts.filter(p => p.author_id === id));
};

const postLoader = new DataLoader(batchPostsByAuthorIds);

const resolvers = {
  User: {
    posts: (parent) => postLoader.load(parent.id)
  }
};

DataLoader batches multiple load calls into a single query and caches results within a single request.

Mutations and Input Types

mutation CreateNewUser {
  createUser(input: { name: "Charlie", email: "charlie@example.com", role: MEMBER }) {
    id
    name
    email
    role
  }
}

Mutation Response Pattern

Return the created or modified object along with any status information:

type MutationResponse {
  success: Boolean!
  message: String
  user: User
}

type Mutation {
  createUser(input: CreateUserInput!): MutationResponse!
}

Subscriptions for Real-Time Data

const { PubSub } = require("Graphql-subscriptions");
const pubsub = new PubSub();

const typeDefs = `#Graphql
  type Subscription {
    postCreated: Post!
  }
`;

const resolvers = {
  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(["POST_CREATED"])
    }
  }
};

// In createPost mutation resolver
const resolver = {
  Mutation: {
    createPost: (_, { title, content, authorId }) => {
      const post = { id: String(posts.length + 1), title, content, authorId };
      posts.push(post);
      pubsub.publish("POST_CREATED", { postCreated: post });
      return post;
    }
  }
};

Security Best Practices

  1. Depth limiting — Prevent deeply nested queries from overloading the server
  2. Query cost analysis — Assign costs to fields and reject expensive queries
  3. Rate Limiting — Limit requests per client like REST APIs
  4. Authentication — Validate tokens in context, not in individual resolvers
  5. Authorization — Check permissions at the field level
const depthLimit = require("Graphql-depth-limit");

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(7)]
});

Common Errors

  1. N+1 query problem — Making individual database queries for each related object. Use DataLoader to batch and cache queries.

  2. Overly permissive schema — Exposing internal database fields through GraphQL. Design the schema for clients, not the database.

  3. No pagination — Returning all records in a single query. Always paginate list fields. Use Cursor-based pagination (Relay spec) for consistency.

  4. Expensive queries — Allowing clients to request deeply nested data that causes performance issues. Implement query cost analysis and depth limiting.

  5. Ignoring nullability — Marking every field as non-null (String!). A single null field from the database crashes the entire response.

  6. No error handling in resolvers — Throwing raw errors that leak Stack traces. Catch errors and return user-friendly error messages.

  7. Missing authentication context — Checking auth inside every resolver individually. Set up authentication middleware that populates the context once.

Practice Questions

  1. How does GraphQL differ from REST in terms of data fetching?
  2. What is the N+1 Problem and how does DataLoader solve it?
  3. What is the difference between a Query and a Mutation?
  4. How do you implement real-time features in GraphQL?
  5. What security measures should you implement for a GraphQL API?

Challenge

Build a GraphQL API for a project management system. Implement types for Project, Task, User, and Comment. Include queries with filtering and pagination, mutations for CRUD operations, subscriptions for task status changes, DataLoader for batching related data, depth limiting and query cost analysis for security. Deploy with Apollo Server and test with Apollo Sandbox.

FAQ

Is GraphQL a replacement for REST? GraphQL is not a direct replacement. GraphQL is better for complex data requirements, multiple data sources, and rapidly evolving frontends. REST is simpler for straightforward CRUD APIs and works better with HTTP caching.

Can I use GraphQL with existing REST APIs? Yes. You can wrap REST APIs with a GraphQL layer using schema stitching or Apollo RESTDataSource. This lets you gradually adopt GraphQL without rewriting backend services.

Does GraphQL support file uploads? GraphQL does not natively support file uploads. Use multipart request spec or handle file uploads through a separate REST endpoint and reference the file URL in GraphQL mutations.

How do I handle authentication in GraphQL? Pass authentication tokens in the HTTP Authorization header. Apollo Server extracts the token in the context function and makes user data available to all resolvers. Never handle auth inside individual resolvers.

What is the Relay specification? Relay is a GraphQL client framework by Facebook that specifies pagination patterns (Cursor-based), object identification (global IDs), and mutation conventions. Following Relay spec makes your GraphQL API compatible with Relay, Apollo, and other clients.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro