Apollo vs Urql vs Fetch: The Ultimate Showdown

Angular developers often face the challenge of efficiently fetching and managing data from GraphQL APIs. This comprehensive guide dives into five powerful approaches for integrating GraphQL into your Angular applications. We'll explore everything from full-featured client libraries to lightweight solutions, using a practical example of fetching post data to demonstrate each method's strengths and nuances.
Our journey will take us through Apollo Angular, Urql, GraphQL-Request, Axios, and the native Fetch API, each offering unique advantages for different project needs. Whether you're building a small-scale application or a complex enterprise system, this guide will equip you with the knowledge to choose the best GraphQL integration method for your Angular project.
We'll not only cover the implementation details but also delve into error handling strategies, providing you with robust solutions to gracefully manage various API-related issues. By the end of this guide, you'll have a clear understanding of how to leverage GraphQL in Angular, complete with code snippets, real-world analogies, and a detailed comparison table to aid your decision-making process.
So, buckle up and get ready to supercharge your Angular applications with the power of GraphQL!
NB: We are not using the traditional NgModule-based Angular applications instead we will be using the newer standalone component approach; below is the version of angular cli version used throughout the guide.
ng version
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
Angular CLI: 18.0.7
Node: 20.12.2
Package Manager: npm 10.5.0
OS: linux x64
Angular: 18.0.6
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, platform-server
... router
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1800.7
@angular-devkit/build-angular 18.0.7
@angular-devkit/core 18.0.7
@angular-devkit/schematics 18.0.7
@angular/cli 18.0.7
@angular/ssr 18.0.7
@schematics/angular 18.0.7
rxjs 7.8.1
typescript 5.4.5
zone.js 0.14.7
We'll be using a Tailcall backend that wraps the JSONPlaceholder API, providing a GraphQL interface to RESTful data.
🛠️ Project Setup
First, let's set up our Angular project:
ng new angular-graphql-tailcall-showcase
cd angular-graphql-tailcall-showcase
🔧 Tailcall Backend Configuration
Create a tailcall directory in the project root and add a jsonplaceholder.graphql file:
# File: tailcall/jsonplaceholder.graphql
schema
@server(port: 8000, hostname: "0.0.0.0")
@upstream(
baseURL: "http://jsonplaceholder.typicode.com"
httpCache: 42
) {
query: Query
}
type Query {
posts: [Post] @http(path: "/posts")
user(id: Int!): User @http(path: "/users/{{.args.id}}")
}
type User {
id: Int!
name: String!
username: String!
email: String!
phone: String
website: String
}
type Post {
id: Int!
userId: Int!
title: String!
body: String!
user: User @http(path: "/users/{{.value.userId}}")
}
To start the Tailcall server, run:
tailcall start ./tailcall/jsonplaceholder.graphql
1. Apollo Angular - The Luxury Sports Car of GraphQL Clients
First up on our list is Apollo Angular. If GraphQL clients were cars, Apollo would be the Tesla of the bunch - sleek, powerful, and packed with features you didn't even know you needed. Let's pop the hood and see what makes this beauty purr!
Installation and Integration Steps
Before we can take Apollo for a spin, we need to get it set up in our garage (I mean, project). Here's how:
-
Install the necessary packages::
npm install apollo-angular @apollo/client graphql -
Configure Apollo in your
app.config.ts:import { APOLLO_OPTIONS, ApolloModule } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache } from '@apollo/client/core';
// In your ApplicationConfig
{
providers: [
importProvidersFrom(ApolloModule),
{
provide: APOLLO_OPTIONS,
useFactory: (httpLink: HttpLink) => ({
cache: new InMemoryCache(),
link: httpLink.create({
uri: '/graphql',
}),
}),
deps: [HttpLink],
},
],
} -
Code Snippets Now that we've got our Apollo rocket fueled up, let's see it in action! Here's a component that fetches a list of posts using Apollo in
src/app/apollo-angular/post-list.component.ts:
import {Component, OnDestroy} from "@angular/core"
import {CommonModule} from "@angular/common"
import {Apollo, gql} from "apollo-angular"
import {ChangeDetectorRef} from "@angular/core"
import {
catchError,
takeUntil,
mergeMap,
} from "rxjs/operators"
import {Subject, of, throwError} from "rxjs"
@Component({
selector: "app-apollo-post-list",
standalone: true,
imports: [CommonModule],
template: `
<h2>Posts (Apollo Angular)</h2>
<button (click)="fetchPosts()" [disabled]="loading">
{{ loading ? "Loading..." : "Load Posts" }}
</button>
<button (click)="triggerNetworkError()">
Trigger Network Error
</button>
<button (click)="triggerGraphQLError()">
Trigger GraphQL Error
</button>
<button (click)="triggerUnexpectedError()">
Trigger Unexpected Error
</button>
<ul *ngIf="!error">
<li *ngFor="let post of posts">{{ post.title }}</li>
</ul>
<div *ngIf="error" class="error-message">
{{ error }}
</div>
`,
styles: [
`
.error-message {
color: red;
margin-top: 10px;
}
`,
],
})
export class ApolloPostListComponent implements OnDestroy {
// ... (component properties and constructor)
fetchPosts() {
this.loading = true
this.error = null
this.posts = []
let query = gql`
query GetPosts($limit: Int) {
posts(limit: $limit) {
id
title
${this.simulateGraphQLError ? "nonExistentField" : ""}
}
}
`
this.apollo
.watchQuery({
query: query,
variables: {
limit: 10,
},
})
.valueChanges.pipe(
takeUntil(this.unsubscribe$),
mergeMap((result) => {
if (this.simulateNetworkError) {
return throwError(
() => new Error("Simulated network error"),
)
}
if (this.simulateUnexpectedError) {
throw new Error("Simulated unexpected error")
}
return of(result)
}),
catchError((error) => {
this.handleError(error)
return of(null)
}),
)
.subscribe({
next: (result: any) => {
if (result) {
this.posts = result.data?.posts || []
}
this.loading = false
this.cdr.detectChanges()
},
error: (error) => this.handleError(error),
complete: () => {
this.loading = false
this.cdr.detectChanges()
},
})
}
// ... (error handling and simulation methods)
}
Wow, would you look at that beauty? 😍 This component is like a finely tuned engine, ready to fetch your posts with the precision of a Swiss watch. Let's break down what's happening here:
- We're using Apollo's watchQuery method to fetch our posts. It's like having a personal assistant who's always on the lookout for the latest data.
- We've got some nifty error simulation methods. It's like having a crash test dummy for your data fetching - you can deliberately cause errors to see how your app handles them. Safety first, right?
- The mergeMap operator is our traffic controller, deciding whether to let the data through or throw an error based on our simulation flags.
- We're using takeUntil with a Subject to ensure we clean up our subscriptions when the component is destroyed. It's like having an eco-friendly car that doesn't leave any pollution (memory leaks) behind!
- The template gives us a simple UI to fetch posts and trigger various error scenarios. It's like having a dashboard with different buttons to test your car's performance.
Error Handling
Speaking of errors, Apollo doesn't just fetch data - it's got your back when things go wrong. Check out this error handling logic:
private handleError(error: any) {
this.loading = false;
if (error.networkError) {
this.error = 'Network error. Please check your internet connection.';
} else if (error.graphQLErrors) {
this.error = `GraphQL error: ${error.graphQLErrors
.map((e: { message: any }) => e.message)
.join(', ')}`;
} else {
this.error = 'An unexpected error occurred. Please try again later.';
}
console.error('Error fetching posts', error);
this.cdr.detectChanges();
}
This error handler is like having a built-in mechanic. Whether it's a network issue (like running out of gas) or a GraphQL error (engine trouble), it's got you covered with user-friendly messages.
Wrapping Up Apollo Angular
And there you have it, folks! Apollo Angular - the smooth-riding, feature-packed, error-handling marvel of the GraphQL world. It's like driving a luxury car with a supercomputer onboard.
2. Axios - The Versatile Muscle Car of HTTP Clients
If Apollo Angular is the luxury sports car of GraphQL clients, then Axios is like a classic muscle car - powerful, versatile, and ready to handle anything you throw at it. It might not have all the GraphQL-specific bells and whistles, but boy, can it perform!
1. Installation and Integration Steps
Before we hit the gas, let's get our Axios engine installed and tuned up:
- Installations.
First, rev up your terminal and run:
npm install axios
Unlike Apollo, Axios doesn't need any special configuration in your app.config.ts. It's more of a plug-and-play solution. Just import it where you need it, and you're good to go!
- Code Snippets
Now, below we implement data fetching using axios in src/app/axios-angular/post-list.component.ts:
import {
Component,
OnInit,
ChangeDetectorRef,
} from "@angular/core"
import {CommonModule} from "@angular/common"
import axios, {AxiosInstance, AxiosError} from "axios"
@Component({
selector: "app-axios-post-list",
standalone: true,
imports: [CommonModule],
template: `
<h2>Posts (Axios Angular)</h2>
<button (click)="fetchPosts()" [disabled]="loading">
{{ loading ? "Loading..." : "Load Posts" }}
</button>
<button (click)="triggerNetworkError()">
Trigger Network Error
</button>
<button (click)="triggerGraphQLError()">
Trigger GraphQL Error
</button>
<button (click)="triggerUnexpectedError()">
Trigger Unexpected Error
</button>
<ul *ngIf="!error">
<li *ngFor="let post of posts">{{ post.title }}</li>
</ul>
<div *ngIf="error" class="error-message">
{{ error }}
</div>
`,
// ... (styles omitted for brevity)
})
export class AxiosPostsListsComponent implements OnInit {
private client: AxiosInstance
posts: any[] = []
loading = false
error: string | null = null
// Error simulation flags
private simulateNetworkError = false
private simulateGraphQLError = false
private simulateUnexpectedError = false
constructor(private cdr: ChangeDetectorRef) {
this.client = axios.create({
baseURL: "/graphql",
headers: {
"Content-Type": "application/json",
},
})
}
ngOnInit() {
// Add a request interceptor
this.client.interceptors.request.use(
(config) => {
if (this.simulateNetworkError) {
return Promise.reject(
new Error("Simulated network error"),
)
}
return config
},
(error) => Promise.reject(error),
)
}
private GET_DATA = `
query GetPosts($limit: Int) {
posts(limit: $limit) {
id
title
${this.simulateGraphQLError ? "nonExistentField" : ""}
}
}
`
async query(queryString: string, variables: any = {}) {
try {
if (this.simulateUnexpectedError) {
throw new Error("Simulated unexpected error")
}
const response = await this.client.post("", {
query: queryString,
variables,
})
return response.data
} catch (error) {
this.handleError(error)
throw error
}
}
async fetchPosts() {
this.loading = true
this.error = null
this.posts = []
this.cdr.detectChanges()
try {
const result = await this.query(this.GET_DATA, {
limit: 10,
})
this.posts = result.data.posts
this.loading = false
this.cdr.detectChanges()
} catch (error) {
// Error is already handled in query method
this.loading = false
this.cdr.detectChanges()
}
}
// ... (error handling and simulation methods omitted for brevity)
}
This Axios-powered component is revving up to fetch those posts faster than you can say "GraphQL"! Let's break down what's happening in this high-octane code:
- We're creating an Axios instance in the constructor. It's like customizing your car with a specific paint job (baseURL) and some cool decals (headers).
- The ngOnInit method adds a request interceptor. Think of it as a nitrous oxide system - it can give your requests an extra boost or, in this case, simulate a network error if you want to test your error handling.
- Our query method is like the engine of this muscle car. It takes a GraphQL query string and variables, then fires off the request. If something goes wrong, it calls our trusty mechanic (the handleError method).
- The fetchPosts method is where the rubber meets the road. It calls our query method with the posts query, then updates our component state with the results.
- We've got our error simulation methods, just like in the Apollo example. It's like having different test tracks for your muscle car - you can simulate various error conditions to make sure your code can handle any bumps in the road.