🚀 Apollo Server Setup
A schema on its own is just a menu — nobody's cooking. In this lesson you'll build the kitchen: install Apollo Server 4, hand it your type definitions and resolvers, and watch it turn a written contract into a running GraphQL API you can query from your browser in minutes.
Week 14 · Wednesday: GraphQL · Lecture 2
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Install
@apollo/serverandgraphqland scaffold a Node project for GraphQL - Define a schema with
typeDefsand back every field with a matching resolver - Run a server two ways:
startStandaloneServerfor quick starts andexpressMiddlewarefor real apps - Explain the four resolver arguments —
parent,args,context,info - Use the context to share auth and data sources, and resolve relationships between types
Estimated Time: 65 minutes
Practice: Build a small books-and-authors API and query it from the Apollo Sandbox.
In This Lesson
What Apollo Server Is
You've seen why GraphQL exists. Apollo Server is the most popular way to actually run one in Node.js. It's an open-source, spec-compliant GraphQL server that takes two ingredients — a schema and a set of resolver functions — and gives you back a fully working endpoint, complete with an in-browser IDE for exploring it.
Think of it as a request pipeline: a query arrives, Apollo parses and validates it against your schema, walks the query field by field calling your resolvers, assembles the result, and sends it back. You supply the two ingredients; Apollo runs everything in between.
the schema"] AS --> R["resolvers
the functions"] R --> DB["Database"] R --> API["REST APIs"] R --> MS["Microservices"] AS --> RESP["Shaped JSON response"]
⚠️ We're using Apollo Server 4 (current)
Older tutorials import from a single apollo-server package and call server.listen(). That's Apollo Server 2/3 and is now deprecated. Apollo Server 4 ships as @apollo/server with a separate startStandaloneServer helper. All the code here uses the modern API.
Installing & Scaffolding
Spin up a fresh project. We use ES modules ("type": "module") because that's the modern default and Apollo's docs assume it.
# Create and enter a project folder
mkdir apollo-books && cd apollo-books
# Initialise a Node project
npm init -y
# Install Apollo Server 4 and the graphql runtime it depends on
npm install @apollo/server graphql
Open package.json and add "type": "module" so you can use import syntax:
{
"name": "apollo-books",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@apollo/server": "^4.11.0",
"graphql": "^16.9.0"
}
}
💡 Why two packages?
graphql is the reference implementation of the GraphQL spec — it parses and executes queries. @apollo/server is the HTTP layer and developer tooling built on top of it. Apollo lists graphql as a peer dependency, so you always install both.
Your First Server
Create index.js. This is a complete, runnable GraphQL server — schema, data, resolvers, and startup — in about 30 lines.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// 1. The schema, written in SDL as a template string
const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
`;
// 2. Some in-memory data to stand in for a database
const books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: '3', title: '1984', author: 'George Orwell', year: 1949 },
];
// 3. Resolvers: one function per queryable field
const resolvers = {
Query: {
books: () => books,
book: (_parent, args) => books.find((b) => b.id === args.id),
},
};
// 4. Create the server and start listening
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server ready at ${url}`);
Run it:
node index.js
Output
🚀 Server ready at http://localhost:4000/
Open http://localhost:4000/ in your browser and you'll land in Apollo Sandbox — a full GraphQL IDE with schema docs and autocomplete. Try running:
query {
books {
title
year
}
}
🍽️ The restaurant analogy
typeDefs are the menu — every dish (type) and its ingredients (fields). Resolvers are the kitchen staff who know how to actually make each dish when it's ordered. Apollo Server is the restaurant that seats guests and carries plates. Apollo Sandbox is the front-of-house terminal where you browse the menu and place orders. A menu with no cooks serves nothing — you need both halves.
Anatomy: typeDefs + resolvers
The whole framework rests on one rule: the resolver map mirrors the schema. For every field a client can ask for, there is a function that returns its value. Where you don't write a resolver, Apollo falls back to a default that simply reads the property of the same name off the parent object.
Here's a richer schema with a relationship — a Book has an Author, and an Author has books:
type Book {
id: ID!
title: String!
author: Author! # a relationship, not just a string
genre: String
year: Int
}
type Author {
id: ID!
name: String!
bio: String
books: [Book!]!
}
type Query {
books(genre: String): [Book!]!
book(id: ID!): Book
authors: [Author!]!
author(id: ID!): Author
}
The Resolver Signature
Every resolver receives the same four arguments, in the same order. Memorise them — they're the vocabulary of GraphQL on the server.
const resolvers = {
Query: {
book: (parent, args, context, info) => {
// parent → the result of the parent resolver (root is usually unused here)
// args → the field's arguments, e.g. { id: "123" }
// context → shared per-request object (auth, data sources, loaders)
// info → low-level details about the query's execution (rarely needed)
return findBookById(args.id);
},
},
};
| Argument | What it holds | You'll use it for |
|---|---|---|
parent | The object returned by the resolver one level up | Resolving nested fields — a Book.author reads parent.authorId |
args | The arguments passed to this field | Reading id, filters, pagination inputs |
context | A shared object built once per request | Auth (the current user), DB connections, DataLoaders |
info | The query AST and execution metadata | Advanced cases — field selection, caching hints |
💡 Destructure what you need
In practice you rarely use all four. It's idiomatic to skip parent with an underscore and destructure args and context: book: (_, { id }, { user }) => .... That reads far more clearly than positional access.
Resolving Relationships
This is where GraphQL's graph nature clicks. When a query asks for book { author { name } }, Apollo first resolves the book, then passes that book as the parent into the Book.author resolver. Each type gets its own resolver block.
const books = [
{ id: '1', title: 'The Great Gatsby', authorId: '1', genre: 'FICTION', year: 1925 },
{ id: '2', title: '1984', authorId: '2', genre: 'SCIFI', year: 1949 },
];
const authors = [
{ id: '1', name: 'F. Scott Fitzgerald', bio: 'American novelist' },
{ id: '2', name: 'George Orwell', bio: 'English novelist' },
];
const resolvers = {
Query: {
books: (_, { genre }) =>
genre ? books.filter((b) => b.genre === genre) : books,
book: (_, { id }) => books.find((b) => b.id === id),
authors: () => authors,
author: (_, { id }) => authors.find((a) => a.id === id),
},
// How to turn a Book's authorId into a full Author object
Book: {
author: (book) => authors.find((a) => a.id === book.authorId),
},
// The reverse direction: all books written by an author
Author: {
books: (author) => books.filter((b) => b.authorId === author.id),
},
};
Notice the storage detail: books hold an authorId, but the schema exposes a full author object. The Book.author resolver bridges that gap. Clients never see the foreign key — they just traverse the relationship.
⚠️ This is where the N+1 problem hides
Querying 100 books each with their author calls Book.author 100 times. Against a real database that's 100 separate lookups. We'll batch them away with DataLoader in the next lesson — for now, just know the resolver-per-field model is what creates the risk.
Context: Auth & Data Sources
The context is a fresh object Apollo builds for every request and hands to every resolver. It's the right place for anything request-specific: the authenticated user, database clients, and per-request caches. In Apollo Server 4 you provide it via the context option to startStandaloneServer.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
// context runs once per request, before any resolver
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '') || '';
const user = await getUserFromToken(token); // null if not signed in
return { user };
},
});
Resolvers then read from it as their third argument — the ideal spot for authorization checks:
const resolvers = {
Query: {
// Public data — anyone may read
books: () => books,
},
Mutation: {
addBook: (_, { input }, { user }) => {
// Guard the write with the context's user
if (!user) {
throw new Error('You must be signed in to add a book');
}
const book = { id: String(books.length + 1), ...input };
books.push(book);
return book;
},
},
};
✅ Keep secrets out of args, put them in context
Never pass a user's identity as a query argument — a client could lie about it. Derive it server-side from the request's token inside the context function, and resolvers can trust context.user completely.
Mounting on Express
startStandaloneServer is perfect for getting going, but real apps need more: a REST route or two, custom middleware, health checks, CORS control. For that, Apollo Server 4 gives you expressMiddleware, which mounts the GraphQL handler onto an Express app you own.
npm install express cors @apollo/server graphql
import express from 'express';
import cors from 'cors';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
const app = express();
// A plain Express route, alongside GraphQL — great for load balancers
app.get('/health', (_req, res) => res.status(200).send('OK'));
const server = new ApolloServer({ typeDefs, resolvers });
await server.start(); // must start before mounting the middleware
app.use(
'/graphql',
cors(),
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({ user: await getUserFromToken(req) }),
})
);
app.listen(4000, () => {
console.log('🚀 GraphQL ready at http://localhost:4000/graphql');
});
💡 Which should I use?
Start with startStandaloneServer while learning or prototyping — it's one call and you're live. Switch to expressMiddleware the moment you need to share the process with other HTTP routes or plug into existing Express middleware. The typeDefs, resolvers, and context logic carry over unchanged.
Practice & Quiz
🏋️ Exercise 1: Add a filtered query
Goal: Extend the books schema with a booksByYear(year: Int!) query that returns every book published in a given year, and write its resolver against the in-memory books array.
💡 Hint
Add the field to type Query first, then add a matching function under resolvers.Query. Read the year from the args object (destructure it) and filter the array.
✅ Solution
# In typeDefs, inside type Query:
booksByYear(year: Int!): [Book!]!
// In resolvers.Query:
booksByYear: (_, { year }) => books.filter((b) => b.year === year),
The schema declares the contract (a non-null list of non-null books); the resolver fulfils it.
🏋️ Exercise 2: Resolve a computed field
Goal: Add a non-stored field displayTitle to Book that returns the title with the year in parentheses, e.g. "1984 (1949)". There is no displayTitle property in the data — you must compute it in a resolver.
✅ Solution
# In typeDefs, inside type Book:
displayTitle: String!
// In resolvers, add a Book block:
Book: {
displayTitle: (book) => `${book.title} (${book.year})`,
},
This is the power of field resolvers: schema fields don't have to map to stored columns — they can be computed from the parent on the fly.
🎯 Quick Quiz
Question 1: In Apollo Server 4, which import gives you the quickest way to run a server?
Question 2: What is the correct order of a resolver's four arguments?
Question 3: Where should the currently authenticated user come from?
Best Practices & Pitfalls
✅ Do
- Use the current
@apollo/server(v4) packages andawait server.start()before mounting middleware - Keep resolvers thin — delegate real logic to service functions and data sources
- Build auth and shared clients once per request in the
contextfunction - Destructure
argsandcontextfor readable resolvers:(_, { id }, { user }) => ...
❌ Don't
- Copy Apollo Server 2/3 snippets that
require('apollo-server')and callserver.listen() - Put database queries or heavy logic directly inline in every resolver
- Trust identity passed as an argument instead of reading it from context
- Forget the inner
!in list types —[Book!]!and[Book]mean different things
⚠️ The #graphql comment is worth adding
Starting your typeDefs template string with `#graphql is a magic comment that tells editors and the GraphQL VS Code extension to syntax-highlight the SDL inside the string. It's a comment to GraphQL itself, so it's harmless — but it makes schemas far easier to read.
Summary
🎉 Key Takeaways
- Apollo Server 4 turns typeDefs + resolvers into a running GraphQL endpoint
- Install
@apollo/serverandgraphql; start fast withstartStandaloneServer - The resolver map mirrors the schema — one function per queryable field
- Every resolver gets
(parent, args, context, info); relationships resolve via theparent - The context carries per-request auth and data sources; graduate to
expressMiddlewarefor real apps
📚 Additional Resources
- Apollo Server — Getting Started
- Apollo Server — Resolvers
- Apollo Server — Context & contextValue
- graphql.org — Execution & Resolvers
🚀 What's Next?
Your server can answer questions — now let's make it hold richer conversations. In Queries and mutations you'll add arguments, variables, and write operations, design mutation payloads, and finally slay the N+1 problem with DataLoader.
🎉 Your API is live!
You went from an empty folder to a queryable GraphQL server with relationships and auth-aware context. That's the backbone every GraphQL feature builds on.