troubleshooting2025-02-05Β·12Β·210/348

Fixing GraphQL Server Setup Errors in Node.js Applications

Common GraphQL server configuration mistakes, resolver patterns, and schema-first vs code-first approaches with practical solutions for Apollo Server and Yoga.

Introduction

GraphQL servers are deceptively simple to set up but tricky to configure correctly for production. After building five different GraphQL APIs for clients over the past two years, I have collected a comprehensive list of the configuration errors that cause the most headaches. This post covers the errors I encounter most frequently, from missing resolvers to N+1 query problems, and the patterns that prevent them.

The examples use Apollo Server 4 and GraphQL Yoga, but the concepts apply to any GraphQL implementation in Node.js.

Environment

node --version
# v20.11.0

npm list apollo-server graphql
# apollo-server@4.10.0
# graphql@16.8.1

npm list @graphql-tools/schema
# @graphql-tools/schema@10.0.0

The server structure:

graphql-api/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ schema/
β”‚   β”‚   β”œβ”€β”€ typeDefs.ts
β”‚   β”‚   └── resolvers.ts
β”‚   β”œβ”€β”€ context/
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ datasources/
β”‚   β”‚   β”œβ”€β”€ UserAPI.ts
β”‚   β”‚   └── PostAPI.ts
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   └── auth.ts
β”‚   └── index.ts
β”œβ”€β”€ tests/
β”‚   └── schema.test.ts
└── package.json

Problem

The server started successfully but returned incorrect data for nested queries. Specifically, the user.posts resolver was being called once per user in a list, causing an N+1 query problem that made the API unusable with more than 100 users.

# This query triggered N+1 problem
query {
  users {
    id
    name
    posts {
      id
      title
    }
  }
}

# Server logs showed:
# [SQL] SELECT * FROM users; -- 1 query
# [SQL] SELECT * FROM posts WHERE user_id = 1; -- N queries
# [SQL] SELECT * FROM posts WHERE user_id = 2;
# [SQL] SELECT * FROM posts WHERE user_id = 3;
# ... repeated for each user

The second issue was that the DateTime scalar type caused serialization errors when clients sent ISO date strings. Apollo Server's default scalar types do not handle custom formats without explicit configuration.

The third issue was that the GraphQL schema accepted queries but returned a 400 error for mutations with optional arguments:

# This mutation failed
mutation {
  createPost(input: { title: "Hello" }) {
    id
  }
}

# Error: Field "content" of required type "String!" was not provided

Analysis

The N+1 problem occurs because GraphQL resolvers execute independently for each field. Without DataLoader, each resolver makes its own database query. The resolver definition itself was correct, but the execution model requires batching.

The DateTime serialization issue stems from GraphQL's type system. The String scalar accepts any string, but when you need to validate and transform dates, you must define a custom scalar type. Apollo Server 4 changed how custom scalars are registered compared to version 3, which caused confusion during a recent upgrade.

The mutation failure was caused by a schema definition error. The content field was marked as required (String!) in the input type but should have been optional:

# Incorrect schema
input CreatePostInput {
  title: String!
  content: String!    # Should be String (optional)
  tags: [String!]
}

# The resolver expected optional content
# but the schema required it

Solution

Fix the N+1 problem using DataLoader:

// src/dataloaders/UserPostsLoader.ts
import DataLoader from "dataloader";
import { PostAPI } from "../datasources/PostAPI";

export function createPostsByUserLoader(postAPI: PostAPI) {
  return new DataLoader(async (userIds) => {
    // Single batch query instead of N individual queries
    const posts = await postAPI.getPostsByUserIds(userIds as string[]);

    // Group posts by userId
    const postsByUser = new Map();
    for (const userId of userIds) {
      postsByUser.set(userId, []);
    }

    for (const post of posts) {
      const userPosts = postsByUser.get(post.userId);
      if (userPosts) {
        userPosts.push(post);
      }
    }

    // Return in the same order as the input IDs
    return userIds.map((id) => postsByUser.get(id as string) || []);
  });
}

Update the context to include the DataLoader:

// src/context/index.ts
import { createPostsByUserLoader } from "../dataloaders/UserPostsLoader";
import { PostAPI } from "../datasources/PostAPI";

export interface Context {
  dataSources: {
    userAPI: UserAPI;
    postAPI: PostAPI;
  };
  loaders: {
    postsByUser: DataLoader;
  };
  userId: string | null;
}

export async function createContext({ req }: { req: Request }): Promise {
  const postAPI = new PostAPI();

  return {
    dataSources: {
      userAPI: new UserAPI(),
      postAPI,
    },
    loaders: {
      postsByUser: createPostsByUserLoader(postAPI),
    },
    userId: extractUserId(req),
  };
}

Fix the resolver to use DataLoader:

// src/schema/resolvers.ts
export const resolvers = {
  User: {
    posts: async (parent: User, _: unknown, context: Context) => {
      // Use DataLoader instead of direct database query
      return context.loaders.postsByUser.load(parent.id);
    },
  },
};

Define the DateTime custom scalar for Apollo Server 4:

// src/schema/scalars.ts
import { GraphQLScalarType, Kind } from "graphql";

export const DateTimeScalar = new GraphQLScalarType({
  name: "DateTime",
  description: "ISO 8601 date-time string",
  serialize(value: unknown) {
    if (value instanceof Date) {
      return value.toISOString();
    }
    throw new Error("DateTime serializer expected a Date object");
  },
  parseValue(value: unknown) {
    if (typeof value === "string") {
      const date = new Date(value);
      if (isNaN(date.getTime())) {
        throw new Error(`Invalid DateTime: ${value}`);
      }
      return date;
    }
    throw new Error("DateTime parser expected a string");
  },
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      const date = new Date(ast.value);
      if (isNaN(date.getTime())) {
        throw new Error(`Invalid DateTime: ${ast.value}`);
      }
      return date;
    }
    throw new Error("DateTime can only parse string literals");
  },
});

Fix the mutation schema to make content optional:

# Correct schema
input CreatePostInput {
  title: String!
  content: String           # Optional - no exclamation mark
  tags: [String!]
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
}

Configure Apollo Server with all fixes:

// src/index.ts
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { typeDefs } from "./schema/typeDefs";
import { resolvers } from "./schema/resolvers";
import { DateTimeScalar } from "./schema/scalars";
import { createContext } from "./context";

const server = new ApolloServer({
  typeDefs: [typeDefs, DateTimeScalar],
  resolvers,
  introspection: process.env.NODE_ENV !== "production",
  formatError: (formattedError) => {
    console.error("GraphQL Error:", formattedError);
    return {
      ...formattedError,
      message: formattedError.message,
    };
  },
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: createContext,
});

console.log(`Server ready at ${url}`);

Lessons Learned

  1. Always use DataLoader for any field that resolves a collection. Even if your current dataset is small, the N+1 problem becomes severe as data grows.

  2. Test your schema with query complexity analysis. Set up query depth limits and complexity costs to prevent abuse.

  3. Custom scalars need explicit parsing logic. Do not rely on string coercion for date or number fields.

  4. Apollo Server 4 changed the context setup significantly. If you are upgrading from v3, read the migration guide carefully.

  5. Use schema-first development for teams with separate frontend and backend developers. The schema becomes the contract that both sides implement against.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.