Hapi.js is a strong Node.js framework that is great for creating enterprise-level APIs with its configuration-focused design, built-in security features, and wide range of plugins.
When paired with GraphQL, Hapi.js becomes a powerful foundation for building complex, scalable APIs that provide accurate data fetching while retaining the framework's well-known reliability and developer experience.
This detailed tutorial guides you through building production-ready GraphQL APIs with Hapi.js and modern GraphQL tools, emphasizing real-world patterns and best practices.
Prerequisites
Before starting, make sure you have Node.js 18 or a newer version installed on your system. This guide assumes you are familiar with JavaScript ES6+ features, async/await patterns, and basic API development concepts.
Setting up your Hapi.js GraphQL project
Following along with hands-on examples will maximize your learning experience, so let's set up a new Hapi.js project where you can try out each concept as we go.
Start by creating a specific directory for your project and initializing the Node.js environment.
Install the essential dependencies for GraphQL with Apollo Server:
Let's examine what each package contributes to our setup:
@apollo/server: Apollo Server v4+ with built-in standalone server capabilities.graphql: The reference implementation of GraphQL for JavaScript, handling schema parsing and query execution.
Create a server.js file in your project root and establish the foundational server structure:
This implementation uses Apollo Server's built-in standalone server, which provides a simple, reliable way to run a GraphQL API without requiring additional framework integrations. The standalone server includes built-in CORS support and request handling, making it perfect for getting started quickly.
The configuration demonstrates Hapi's declarative approach - we specify server parameters and plugin registration through clear, object-based configuration rather than imperative middleware chains.
Add a development script to your package.json to streamline the development workflow:
Launch your GraphQL server using the development command:
Navigate to http://localhost:4000/graphql in your browser to access Apollo Studio, an advanced GraphQL IDE that provides schema exploration, query composition, and debugging capabilities:
Execute this query to validate your setup works correctly:
You should receive a structured response:
Apollo Studio's interface offers intelligent features, including autocomplete, real-time schema validation, and comprehensive documentation generation.
These capabilities significantly enhance the development experience by making API exploration and testing more intuitive and efficient.
Designing GraphQL schemas
A GraphQL schema is the core contract between your API and its users. It defines all available operations, data types, and how they relate. Unlike REST, which relies on multiple endpoints, GraphQL uses a single, well-structured schema to describe the entire API.
Schemas are made up of types, fields, arguments, and operations like queries and mutations. Understanding these pieces is key to designing clean, scalable APIs.
Let's create a more realistic schema. Start by creating a new file called models.js to define our data types.
These type definitions highlight key GraphQL concepts. Author and Book show how to create object types with scalar fields (like ID and String) and relationships to other types. The use of exclamation marks (!) indicates required fields, while their absence denotes optional fields that may return null values.
We're also using relationships throughout. The books field on Author returns a list of books, while the author field on Book returns the related author. This demonstrates GraphQL's power to traverse relationships and return exactly the data requested by the client.
Next, let's define input types for mutations. Input types are used to pass structured arguments into queries and mutations. Add the following to your models.js file:
Input types offer several advantages over using individual arguments: they group related parameters together, make mutations more readable, and facilitate easier validation and transformation.
Update your server.js file to include these new types and provide some sample data to work with:
This enhanced schema demonstrates several important query patterns. The books and authors fields return lists of items, while book and author provide single-item lookups by ID. The modular approach separates type definitions from query definitions, making the code more maintainable.
If you try to test your enhanced schema with this query:
You might encounter an error like "Cannot return null for non-nullable field Book.author." This happens because our schema defines the author field as required (Author!) but we haven't implemented the relationship resolution yet. Let's add resolver methods to handle these relationships properly:
Now, when you run the same query:
You'll see the author information populated for each book:
This demonstrates GraphQL's power to traverse relationships and return exactly the data requested by the client.
The error handling in the resolver ensures that if there are any data integrity issues, they're caught and reported clearly rather than causing silent failures.
Implementing mutations for data modification
While queries handle data retrieval, mutations manage data modifications such as creating, updating, or deleting resources. GraphQL mutations provide a structured approach to state changes while maintaining the same type safety and flexibility as queries.
Mutations are particularly powerful because they can return complex objects that include both the modified data and additional context like validation errors or related information that changed as a result of the operation.
Let's add comprehensive mutation capabilities to our API. First, add a Mutation type to your query definitions:
The mutation resolvers demonstrate several important patterns. They accept input objects rather than individual parameters, which keeps the GraphQL schema clean and makes validation easier. They perform validation before making changes - the createBook mutation verifies that the referenced author exists before creating the book.
Notice how we generate new IDs by finding the maximum existing ID and incrementing it. In a real application, you'd typically let your database handle ID generation, but this approach works well for our demonstration.
Test your mutations with these GraphQL operations:
Now try creating a book for the new author:
The mutation system offers a clear interface for data changes while keeping the same type safety and introspection features that make GraphQL queries so powerful.
Clients can specify exactly what data they want returned after the mutation finishes, which is especially useful for updating UI components efficiently.
Final thoughts
You've successfully built a GraphQL API using Apollo Server, covering essential concepts from basic schema design to advanced mutations and relationship handling. This foundation demonstrates GraphQL's power to provide flexible, type-safe APIs that can evolve with your application's needs.
For further exploration, the Apollo Server documentation offers comprehensive guides on advanced topics, while the GraphQL specification provides deeper insights into the query language itself.