Skip to main content
GraphQL Fundamentals·Lesson 3 of 5

Building a GraphQL Server

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 graphql

Your 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) { ... }
ArgumentWhat it contains
parentThe resolved value of the parent object
argsArguments passed to this field in the query
contextShared data (auth user, database connection)
infoQuery 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

ApproachDescriptionTooling
Schema-firstWrite SDL (.graphql files), generate resolversApollo Server, graphql-tools
Code-firstDefine schema in code, SDL auto-generatedTypeGraphQL, 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
    }
  }
}