๐ŸŒ Detecting your locationโ€ฆ
๐Ÿ“ข Advertisement โ€” Configure AdSense in Appearance โ†’ Customize โ†’ AdSense Settings

How to Build a GraphQL API with Node.js and Apollo Server in 2026

โฑ๏ธ4 min read  ยท  703 words

GraphQL lets clients request exactly the data they need in a single request โ€” no over-fetching, no under-fetching. Combined with Apollo Server and TypeScript, it’s a powerful way to build flexible APIs. This guide builds a complete GraphQL API from scratch.

GraphQL vs REST โ€” Quick Comparison

Aspect REST GraphQL
Data fetching Multiple endpoints Single endpoint, precise queries
Over-fetching Common Eliminated
Versioning URL versions (v1, v2) Schema evolution
Best for Simple CRUD, caching Complex, nested, client-driven data

Project Setup

mkdir graphql-api && cd graphql-api
npm init -y
npm install @apollo/server graphql
npm install -D typescript tsx @types/node
npx tsc --init

Define the Schema

// src/schema.ts
export const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

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

  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!
    publishPost(id: ID!): Post!
  }
`;

Write Resolvers

// src/resolvers.ts
import { db } from './db';

export const resolvers = {
  Query: {
    users: () => db.users.findAll(),
    user: (_: any, { id }: { id: string }) => db.users.findById(id),
    posts: () => db.posts.findAll(),
  },

  Mutation: {
    createUser: (_: any, { name, email }: { name: string; email: string }) => {
      return db.users.create({ name, email });
    },
    createPost: (_: any, args: { title: string; content: string; authorId: string }) => {
      return db.posts.create({ ...args, published: false });
    },
    publishPost: (_: any, { id }: { id: string }) => {
      return db.posts.update(id, { published: true });
    },
  },

  // Field resolvers for relationships
  User: {
    posts: (parent: { id: string }) => db.posts.findByAuthor(parent.id),
  },
  Post: {
    author: (parent: { authorId: string }) => db.users.findById(parent.authorId),
  },
};

Create the Server

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

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

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => {
    // Extract auth token, attach user to context
    const token = req.headers.authorization?.replace('Bearer ', '');
    const user = token ? await verifyToken(token) : null;
    return { user };
  },
});

console.log("GraphQL ready at " + url);

Authentication in Resolvers

Mutation: {
  createPost: (_: any, args, context) => {
    // Require authentication
    if (!context.user) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' },
      });
    }
    return db.posts.create({ ...args, authorId: context.user.id });
  },
},

Solving the N+1 Problem with DataLoader

Field resolvers can trigger N+1 queries (one query per parent). DataLoader batches and caches them:

npm install dataloader
import DataLoader from 'dataloader';

// Create a loader that batches user lookups
const userLoader = new DataLoader(async (ids: readonly string[]) => {
  const users = await db.users.findByIds(ids);
  // Return in the same order as requested ids
  return ids.map(id => users.find(u => u.id === id));
});

// Use in resolver โ€” batches all author lookups into one query
Post: {
  author: (parent, _, context) => context.userLoader.load(parent.authorId),
},

Example Query

# Client requests exactly what it needs
query {
  user(id: "1") {
    name
    posts {
      title
      published
    }
  }
}

# Response contains only requested fields:
# { "data": { "user": { "name": "Alice", "posts": [...] } } }

Frequently Asked Questions

Q: GraphQL or REST for my project?
A: GraphQL for complex, nested, client-driven data (mobile apps, dashboards with varied needs). REST for simple CRUD and when HTTP caching matters most. Many teams use both.

Q: How do I handle file uploads in GraphQL?
A: Use the graphql-upload package, or better, upload files to object storage (S3) via a separate REST endpoint or presigned URL and pass the URL in your GraphQL mutation.

Q: Is GraphQL harder to cache than REST?
A: Yes at the HTTP level (single endpoint, POST requests). Use Apollo Client’s normalized cache on the client and persisted queries or response caching on the server to compensate.

Q: How do I prevent expensive/malicious queries?
A: Add query depth limiting, complexity analysis, and rate limiting. Packages like graphql-depth-limit and graphql-query-complexity protect against deeply nested abusive queries.

Q: Should I use code-first or schema-first?
A: Schema-first (SDL strings, shown here) is clear and language-agnostic. Code-first (Nexus, TypeGraphQL) gives better TypeScript integration. Both work; pick based on team preference.

Conclusion

GraphQL with Apollo Server gives clients precise control over the data they fetch, eliminating over- and under-fetching. The setup here โ€” typed schema, resolvers with relationships, context-based auth, and DataLoader for the N+1 problem โ€” covers the essentials of a production GraphQL API. Add depth limiting and complexity analysis before going live, and pair with Apollo Client for a normalized cache on the frontend.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *