Table of Contents
- Prerequisites
- Setting Up Your Node.js Project
- Understanding GraphQL Schema and Resolvers
- Building Your First GraphQL Server
- Working with Data Sources
- Executing Queries and Mutations
- Advanced Concepts
- Deploying Your GraphQL API
- Conclusion
- References
Prerequisites
Before diving in, ensure you have the following:
- Node.js (v14 or later) and npm (v6 or later) installed. Download from nodejs.org.
- Basic knowledge of JavaScript and Node.js.
- Familiarity with REST APIs (to contrast with GraphQL).
- A code editor (e.g., VS Code) and terminal.
Setting Up Your Node.js Project
Let’s start by creating a new Node.js project and installing the necessary dependencies.
Initializing the Project
First, create a project folder and initialize npm:
mkdir graphql-nodejs-guide
cd graphql-nodejs-guide
npm init -y
This generates a package.json file to manage dependencies.
Installing Dependencies
We’ll use Apollo Server (a production-ready GraphQL server) and Express (a web framework for Node.js) to build our API. Run:
npm install @apollo/server graphql express cors
@apollo/server: The core Apollo Server library.graphql: The official GraphQL JavaScript implementation.express: A minimal web framework to handle HTTP requests.cors: Middleware to enable Cross-Origin Resource Sharing (CORS).
Understanding GraphQL Schema and Resolvers
At the heart of every GraphQL API are two key components: schemas and resolvers.
Defining the Schema with SDL
The schema defines the structure of your API: what data types exist, how they relate, and what queries/mutations clients can execute. It uses the GraphQL Schema Definition Language (SDL).
For example, let’s build a simple “Book Library” API. Create a schema.js file:
// schema.js
const { gql } = require('@apollo/server');
const typeDefs = gql`
# A Book type with id, title, author, and publishedYear
type Book {
id: ID! # Unique identifier (non-nullable)
title: String! # Book title (non-nullable)
author: String! # Author name (non-nullable)
publishedYear: Int # Optional publication year
}
# Query type: Defines read operations
type Query {
books: [Book!]! # Get all books (returns a non-nullable list of Books)
book(id: ID!): Book # Get a single book by ID (nullable if not found)
}
# Mutation type: Defines write operations (create, update, delete)
type Mutation {
addBook(
title: String!
author: String!
publishedYear: Int
): Book! # Add a new book (returns the created Book)
}
`;
module.exports = typeDefs;
Key SDL concepts:
!: Marks a field as non-nullable (the server will throw an error if the value is missing).[Type]: A list ofType(e.g.,[Book!]!= a non-nullable list of non-nullable Books).Query: Entry point for read operations.Mutation: Entry point for write operations.
Writing Resolvers
Resolvers are functions that fetch or compute data for fields defined in the schema. They match the structure of the schema and return data in the format expected by the client.
Create a resolvers.js file:
// resolvers.js
const resolvers = {
// Resolvers for Query type
Query: {
// Fetch all books
books: (parent, args, context) => {
return context.dataSources.bookAPI.getBooks();
},
// Fetch a single book by ID
book: (parent, { id }, context) => {
return context.dataSources.bookAPI.getBookById(id);
}
},
// Resolvers for Mutation type
Mutation: {
// Add a new book
addBook: (parent, { title, author, publishedYear }, context) => {
return context.dataSources.bookAPI.createBook({ title, author, publishedYear });
}
}
};
module.exports = resolvers;
Resolver arguments:
parent(orroot): Result from the parent resolver (used for nested fields).args: Input arguments (e.g.,idinbook(id: ID!)).context: Shared data/utility (e.g., database connections, user auth).
Building Your First GraphQL Server
Now, let’s combine the schema and resolvers into a running Apollo Server.
Setting Up Apollo Server
Create an index.js file (the entry point of your app):
// index.js
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const express = require('express');
const cors = require('cors');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
// Initialize Express app
const app = express();
// Define port (use environment variable or default to 4000)
const PORT = process.env.PORT || 4000;
// Create Apollo Server instance
const server = new ApolloServer({
typeDefs, // Schema
resolvers // Resolvers
});
Connecting Resolvers and Schema
Apollo Server needs to connect to Express to handle HTTP requests. We’ll use expressMiddleware to integrate Apollo with Express:
// index.js (continued)
async function startServer() {
// Start Apollo Server
await server.start();
// Apply Express middleware (CORS, JSON parsing)
app.use(cors());
app.use(express.json());
// Mount Apollo Server on Express
app.use('/graphql', expressMiddleware(server, {
context: async ({ req }) => ({
// Context: Pass data sources, user auth, etc.
dataSources: {
bookAPI: new BookAPI() // We’ll define BookAPI next!
}
})
}));
// Start Express server
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/graphql`);
});
}
// Run the server
startServer();
Starting the Server
Before starting, we need a BookAPI data source to manage our book data. Let’s build that next.
Working with Data Sources
Data sources abstract how your API fetches data (e.g., from a database, API, or in-memory store). Let’s start with a simple in-memory store.
In-Memory Data Store
Create a dataSources/BookAPI.js file:
// dataSources/BookAPI.js
class BookAPI {
constructor() {
// Mock in-memory "database"
this.books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', publishedYear: 1925 },
{ id: '2', title: '1984', author: 'George Orwell', publishedYear: 1949 }
];
}
// Get all books
getBooks() {
return this.books;
}
// Get book by ID
getBookById(id) {
return this.books.find(book => book.id === id);
}
// Create a new book
createBook(book) {
const newBook = { ...book, id: (this.books.length + 1).toString() };
this.books.push(newBook);
return newBook;
}
}
module.exports = BookAPI;
Update index.js to import BookAPI:
// index.js (add at the top)
const BookAPI = require('./dataSources/BookAPI');
Now, start the server:
node index.js
You should see:
Server running at http://localhost:4000/graphql
Connecting to a Database (MongoDB Example)
For production, you’ll want a persistent database. Let’s modify BookAPI to use MongoDB with Mongoose (a MongoDB ODM).
Step 1: Install dependencies
npm install mongoose dotenv
Step 2: Configure MongoDB
Create a .env file to store your MongoDB connection string:
MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.mongodb.net/booklibrary?retryWrites=true&w=majority
Replace <username> and <password> with your MongoDB Atlas credentials (sign up at mongodb.com).
Step 3: Define a Mongoose Model
Create models/Book.js:
// models/Book.js
const mongoose = require('mongoose');
const bookSchema = new mongoose.Schema({
title: { type: String, required: true },
author: { type: String, required: true },
publishedYear: { type: Number }
});
module.exports = mongoose.model('Book', bookSchema);
Step 4: Update BookAPI to Use MongoDB
Modify dataSources/BookAPI.js:
// dataSources/BookAPI.js
const Book = require('../models/Book');
const mongoose = require('mongoose');
require('dotenv').config();
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
class BookAPI {
async getBooks() {
return await Book.find();
}
async getBookById(id) {
return await Book.findById(id);
}
async createBook(book) {
const newBook = new Book(book);
return await newBook.save();
}
}
module.exports = BookAPI;
Restart the server. Now your API uses MongoDB!
Executing Queries and Mutations
Apollo Server includes GraphiQL—an in-browser IDE for testing GraphQL queries. Navigate to http://localhost:4000/graphql to use it.
Fetching Data with Queries
Example 1: Get All Books
Run this query in GraphiQL:
query GetAllBooks {
books {
id
title
author
publishedYear
}
}
Response:
{
"data": {
"books": [
{
"id": "650a1b3d...",
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"publishedYear": 1925
},
// More books...
]
}
}
Example 2: Get a Book by ID
query GetBookById {
book(id: "650a1b3d...") { # Replace with a real ID from your DB
title
author
}
}
Modifying Data with Mutations
Example: Add a New Book
mutation AddBook {
addBook(
title: "To Kill a Mockingbird",
author: "Harper Lee",
publishedYear: 1960
) {
id
title
author
}
}
Response:
{
"data": {
"addBook": {
"id": "650a2c4e...",
"title": "To Kill a Mockingbird",
"author": "Harper Lee"
}
}
}
Testing with GraphiQL
GraphiQL provides auto-complete, syntax highlighting, and a “Docs” tab to explore your schema. Use it to experiment with queries and mutations!
Advanced Concepts
Authentication & Authorization
To secure your API, add JWT authentication. Use express-jwt to validate tokens and pass user data to context:
npm install express-jwt jsonwebtoken
Update index.js to include auth middleware:
const jwt = require('express-jwt');
// Add JWT middleware before Apollo
app.use(
jwt({
secret: process.env.JWT_SECRET, // Store in .env
credentialsRequired: false // Allow unauthenticated requests
}).unless({ path: ['/graphql'] })
);
// Update context to include user
context: async ({ req }) => ({
user: req.user, // Decoded JWT payload
dataSources: { bookAPI: new BookAPI() }
})
Restrict mutations to authenticated users in resolvers:
addBook: (parent, args, context) => {
if (!context.user) throw new Error("Authentication required");
return context.dataSources.bookAPI.createBook(args);
}
Error Handling
Throw custom errors with ApolloError:
npm install @apollo/server/errors
const { ApolloError } = require('@apollo/server/errors');
// In resolver:
book: async (parent, { id }, context) => {
const book = await context.dataSources.bookAPI.getBookById(id);
if (!book) throw new ApolloError("Book not found", "NOT_FOUND");
return book;
}
Subscriptions (Real-Time Data)
For real-time updates (e.g., chat apps), use GraphQL subscriptions with WebSockets. Install:
npm install graphql-ws subscriptions-transport-ws
Apollo Server v4 supports subscriptions via graphql-ws. See the Apollo Subscriptions Docs for setup.
Caching
Apollo Server caches responses by default for repeated queries. For advanced caching, use responseCachePlugin:
const { ApolloServerPluginResponseCache } = require('@apollo/server/plugin/responseCache');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginResponseCache()]
});
Deploying Your GraphQL API
Preparing for Deployment
- Add a
Procfile(for Heroku):web: node index.js - Update
package.jsonto include a start script:"scripts": { "start": "node index.js" } - Use environment variables for sensitive data (e.g.,
MONGODB_URI,JWT_SECRET).
Deploying to Heroku
- Install Heroku CLI:
npm install -g heroku - Login:
heroku login - Create app:
heroku create my-graphql-api - Set env vars:
heroku config:set MONGODB_URI=... JWT_SECRET=... - Deploy:
git push heroku main - Open:
heroku open /graphql
Conclusion
You’ve built a fully functional GraphQL API with Node.js and Apollo Server! You learned how to:
- Define schemas with SDL.
- Write resolvers to fetch data.
- Connect to databases (MongoDB).
- Test with GraphiQL.
- Add authentication and error handling.
- Deploy to production.
GraphQL’s flexibility makes it ideal for modern applications. Explore advanced topics like federation (combining multiple APIs) and persisted queries to scale further!