HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•165 Minutes

Building a GraphQL Server with Node.js

Set up a practical GraphQL server using Node.js, understand the server architecture, define schemas and resolvers, execute queries, and establish a clean application structure.

GraphQL Server Architecture

A production GraphQL server is usually more than a schema and a few resolver functions. A clean architecture separates concerns between transport, GraphQL execution, application services, data access, and infrastructure.

A useful structure is:

HTTP / WebSocket Transport
          ↓
GraphQL Server
          ↓
Schema + Validation
          ↓
Resolvers
          ↓
Application Services
          ↓
Repositories / Data Sources
          ↓
Database / External APIs

For a Node.js application, a possible project structure is:

src/
  graphql/
    schema/
    resolvers/
  services/
  repositories/
  models/
  middleware/
  context/
  config/
  server.js

The exact structure is not mandatory. The important principle is separation of responsibilities.

GraphQL layer

Defines the API contract and coordinates execution.

Service layer

Contains application and business logic.

Repository/data-access layer

Handles persistence and external data access.

Infrastructure layer

Handles database connections, logging, configuration, queues, and other operational concerns.

Keeping these boundaries clear prevents resolver files from becoming large collections of database queries and business rules.

Application Layers
GraphQL layer: Schema + resolvers
Defines and executes the API contract.
Service layer: Business/application logic
Contains reusable domain operations.
Repository layer: Data access
Encapsulates persistence and external data retrieval.
Infrastructure: Database, cache, logging, configuration
Provides operational dependencies used by the application.
Node.js GraphQL Application Architecture
Key Takeaways
  • A production GraphQL server benefits from layered architecture.
  • Resolvers should coordinate rather than contain all business logic.
  • Services provide reusable application behavior.
  • Repositories can isolate persistence details.
Knowledge Check

Where should complex reusable business logic generally live in a layered GraphQL application?

Setting Up a Node.js GraphQL Project

A Node.js GraphQL project needs a runtime, a GraphQL implementation, and a server integration.

One popular approach is to use Apollo Server with Node.js. The exact package versions and setup details can evolve, but the architectural concepts remain the same.

A minimal application conceptually needs:

Node.js
  +
GraphQL implementation
  +
GraphQL server framework
  +
Schema
  +
Resolvers

A simple project might contain:

graphql-app/
  package.json
  src/
    schema.js
    resolvers.js
    server.js

The server entry point creates the GraphQL server, registers the schema and resolvers, and starts the HTTP server.

A minimal schema could be:

type Query {
  hello: String!
}

and the resolver:

const resolvers = {
  Query: {
    hello: () => "Hello, GraphQL!"
  }
};

The important learning objective at this stage is not memorizing a particular framework API. It is understanding the relationship between schema, resolver, server, and client operation.

Minimal GraphQL Server Concept
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = `
  type Query {
    hello: String!
  }
`;

const resolvers = {
  Query: {
    hello: () => "Hello, GraphQL!"
  }
};

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

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

console.log(`GraphQL server running at ${url}`);
💡 The schema defines a hello field on the root Query type. The resolver returns its value. Apollo Server combines the schema and resolver map and exposes the GraphQL API. In a production Node.js application, the standalone setup is often replaced or integrated with the application's existing HTTP framework and middleware stack.
Key Takeaways
  • A Node.js GraphQL server combines a schema with resolver implementations.
  • Apollo Server is one GraphQL server implementation for Node.js.
  • Framework setup can change over time, but the schema-resolver relationship remains fundamental.
Knowledge Check

What two pieces are most directly combined to implement a GraphQL field?

Request Context in Node.js

GraphQL context is commonly used to expose request-scoped dependencies to resolvers.

For example, a request context might contain:

{
  user: authenticatedUser,
  requestId: "req-123",
  userService,
  postService,
  loaders
}

A resolver can then access those values:

const resolvers = {
  Query: {
    me: (_, __, context) => {
      return context.user;
    }
  }
};

Context is especially useful for authentication because authentication middleware can establish the current user before GraphQL execution begins.

It can also hold request-specific DataLoader instances. This is important because DataLoader caches should normally be scoped appropriately to a request rather than accidentally shared across unrelated users.

Avoid putting global mutable state into context simply because context is convenient. Application-wide configuration, immutable service instances, and request-specific information have different lifecycles and should be modeled accordingly.

A clean context design makes dependencies explicit and reduces the temptation to import global mutable variables from everywhere in the resolver layer.

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

const context = async ({ req }) => {
  const user = await authenticateRequest(req);

  return {
    user,
    requestId: req.headers["x-request-id"],
    userService,
    postService
  };
};
💡 The context function creates values associated with the current request. Resolvers can then access authentication information and application services through the context argument. In production, authentication failures, request IDs, and dependency lifecycles should be handled consistently.
Key Takeaways
  • GraphQL context is useful for request-scoped dependencies.
  • Authentication information is commonly exposed through context.
  • Request-specific loaders should have appropriate request lifecycles.
  • Avoid using context as an uncontrolled global-state container.
Knowledge Check

Why is request-scoped context useful for GraphQL authentication?

GraphQL Error Handling

GraphQL errors are different from traditional REST error handling because a response can contain both data and errors.

For example:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "User not found"
    }
  ]
}

The exact result depends on the field's nullability and the execution failure.

GraphQL errors can originate from validation, parsing, variable coercion, resolver execution, authorization, or other layers.

Consider:

type Query {
  user(id: ID!): User
}

Because user is nullable, an execution failure can result in user: null alongside an error.

Now consider:

type Query {
  user(id: ID!): User!
}

The non-null contract changes error propagation. If the resolver cannot produce a valid non-null user, GraphQL can propagate null to a nullable parent field, potentially causing a larger portion of the response to become null.

Production APIs should also avoid leaking sensitive internal information through error messages. Database stack traces, credentials, internal hostnames, and implementation details should not become client-facing errors.

A consistent error classification strategy helps clients distinguish expected business failures from unexpected infrastructure failures.

Do not expose internal errors

GraphQL error messages are part of an API response. Avoid exposing stack traces, database credentials, internal hostnames, or sensitive implementation details to clients.

Key Takeaways
  • GraphQL responses can contain both data and errors.
  • Field nullability affects error propagation.
  • Validation and execution errors have different origins.
  • Production error responses should not leak sensitive implementation details.
Knowledge Check

What can a GraphQL response contain when a resolver encounters an execution error?

Organizing a Production GraphQL Codebase

As a GraphQL application grows, placing everything in one schema file and one resolver file becomes difficult to maintain.

A modular structure can organize GraphQL capabilities by domain:

src/
  modules/
    users/
      user.schema.js
      user.resolvers.js
      user.service.js
      user.repository.js
    products/
      product.schema.js
      product.resolvers.js
      product.service.js
      product.repository.js
  graphql/
    schema.js
    context.js
  infrastructure/
    database.js
    cache.js
  server.js

The exact naming convention is less important than the ownership boundaries.

A users module can own user-related schema definitions, resolvers, services, and repositories. A products module can do the same for products.

The root GraphQL schema can then compose these domain definitions.

This modular approach becomes increasingly valuable when the application has multiple teams, because ownership and change boundaries become clearer.

However, modularity should not mean creating hundreds of tiny files without meaningful boundaries. A module should represent a cohesive domain or capability rather than an arbitrary number of functions.

Suggested Module Boundaries
users: Identity and user operations
Owns user-related schema, business logic, and data access.
products: Catalog operations
Owns product-related GraphQL fields and domain behavior.
orders: Order lifecycle
Owns order creation, updates, cancellation, and related logic.
Key Takeaways
  • Large GraphQL applications benefit from domain-oriented modularization.
  • Schema, resolver, service, and repository responsibilities can be separated.
  • Modules should represent meaningful domain boundaries.
  • Avoid both monolithic files and meaningless fragmentation.
Knowledge Check

What is a major benefit of organizing a large GraphQL codebase by domain modules?