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.
Schema + resolversBusiness/application logicData accessDatabase, cache, logging, configuration- 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.
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.
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}`);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.- 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.
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.
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
};
};- 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.
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.
GraphQL error messages are part of an API response. Avoid exposing stack traces, database credentials, internal hostnames, or sensitive implementation details to clients.
- 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.
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.
Identity and user operationsCatalog operationsOrder lifecycle- 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.
What is a major benefit of organizing a large GraphQL codebase by domain modules?