Apollo Server is the most popular way to build a GraphQL API with Node.js. It handles parsing queries, validating against your schema, running resolvers, and formatting responses.
Setup
mkdir graphql-api && cd graphql-api
npm init -y
npm install @apollo/server graphqlYour First Server
// index.js
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!, year: Int): Book!
}
`;
const books = [
{ id: '1', title: 'Clean Code', author: 'Robert C. Martin', year: 2008 },
{ id: '2', title: 'The Pragmatic Programmer', author: 'Andy Hunt', year: 1999 },
];
let nextId = 3;
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find(b => b.id === id),
},
Mutation: {
addBook: (_, { title, author, year }) => {
const book = { id: String(nextId++), title, author, year }
books.push(book)
return book
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Server ready at ${url}`);Run it: node --experimental-vm-modules index.js
Resolvers in Detail
Every field in your schema has a resolver function with this signature:
fieldName(parent, args, context, info) { ... }| Argument | What it contains |
|---|---|
parent | The resolved value of the parent object |
args | Arguments passed to this field in the query |
context | Shared data (auth user, database connection) |
info | Query AST and execution metadata |
Context — Sharing Auth Across Resolvers
Pass shared data like the authenticated user through context:
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = req.headers.authorization?.split(' ')[1]
const user = token ? verifyToken(token) : null
return { user, db }
},
listen: { port: 4000 },
});const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) throw new Error('Not authenticated')
return user
}
}
}Testing with GraphiQL
Apollo Server ships a built-in browser IDE at http://localhost:4000. You can write queries, run them, and explore the schema — no extra tooling needed during development.
Try running these against the server above:
query AllBooks {
books {
id
title
author
year
}
}
mutation AddBook {
addBook(title: "Refactoring", author: "Martin Fowler", year: 2018) {
id
title
}
}Schema-First vs Code-First
| Approach | Description | Tooling |
|---|---|---|
| Schema-first | Write SDL (.graphql files), generate resolvers | Apollo Server, graphql-tools |
| Code-first | Define schema in code, SDL auto-generated | TypeGraphQL, Pothos |
Schema-first (what we've used here) is more explicit and makes the SDL the source of truth. Code-first is preferred in TypeScript projects where you want type safety across schema and resolvers.
Error Handling
Throw GraphQLError for user-facing errors with proper error codes:
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
book: (_, { id }) => {
const book = books.find(b => b.id === id)
if (!book) throw new GraphQLError('Book not found', {
extensions: { code: 'NOT_FOUND' }
})
return book
}
}
}