| |

Angular 119 🅰️ Angular and GraphQL

⏱️ Estimated read: 13 min | |

GraphQL is a query language for APIs and a server-side runtime that lets clients request exactly the data they need, and nothing more. Where REST exposes multiple endpoints that return fixed data shapes, GraphQL exposes a single endpoint and a strongly-typed schema, and the client specifies which fields it wants in the query itself. The server resolves those fields against whatever data sources it uses — databases, microservices, or other APIs — and returns a JSON response that mirrors the query’s shape.

This chapter covers how GraphQL differs from REST, the schema and type system, queries, mutations, and subscriptions, the Apollo Angular client setup, using watchQuery for reactive data, mutations and cache updates, code generation for type safety, and the patterns for structuring GraphQL operations in Angular services.

Key point: GraphQL uses a single endpoint and a typed schema. Clients send queries specifying exact fields; the server resolves them. In Angular, Apollo Angular provides a watchQuery method that returns an observable which updates when the cache changes, and mutate for writes. Code generation with @graphql-codegen/typescript-apollo-angular produces typed services that replace manual interfaces.


Why GraphQL exists alongside REST

The over-fetching problem. A REST endpoint returns a fixed representation. If the UI needs only a product’s title and price, a REST endpoint that returns the full product object — description, images, inventory, categories — sends data the client does not use. GraphQL lets the client specify exactly which fields to return, eliminating over-fetching.

The under-fetching problem. A UI that needs a user and their posts may require two REST calls: one to /users/:id and one to /users/:id/posts. GraphQL allows a single query that traverses the relationship between User and Post, returning both in one request.

The versioning problem. REST APIs evolve by adding new endpoints or versioning the API (/v2/). GraphQL schemas evolve by adding fields; existing queries continue to work, and deprecated fields can be removed once usage declines. There is no need for parallel versions of the API.

The tooling problem. GraphQL schemas are introspectable. Tools can query the schema to discover available types and fields, enabling autocompletion, validation, and automatic documentation. This makes the API self-documenting in a way REST is not.

The client-specified response problem. In REST, the server decides what data a resource returns. In GraphQL, the client decides. This is a fundamental shift: the API describes capabilities, and the client selects what it needs.


a. The schema and type system

A GraphQL service is defined by a schema written in Schema Definition Language (SDL). The schema declares the types that exist, their fields, and the operations that can be performed.

type Post {
  id: ID!
  title: String!
  content: String
  author: Author!
  createdAt: String!
}

type Author {
  id: ID!
  name: String!
  email: String
  posts: [Post!]!
}

type Query {
  posts: [Post!]!
  post(id: ID!): Post
  authors: [Author!]!
}

type Mutation {
  createPost(title: String!, content: String!): Post!
  deletePost(id: ID!): Boolean!
}

The exclamation mark marks a field as non-nullable. The Query type defines read operations; the Mutation type defines write operations. A Subscription type defines real-time operations where the server pushes updates to the client.

The schema is the contract between client and server. Clients can introspect it to discover available operations, and tools use it to validate queries before they are sent.


b. Queries, mutations, and variables

A GraphQL query mirrors the shape of the response. The client specifies the fields it wants, and the server returns only those fields.

query GetPosts {
  posts {
    id
    title
    author {
      name
    }
  }
}

The response contains exactly the requested fields:

{
  "data": {
    "posts": [
      {
        "id": "1",
        "title": "Hello GraphQL",
        "author": { "name": "Alice" }
      }
    ]
  }
}

Variables separate the query structure from the input values, making queries reusable and validated independently of the data:

query GetPostsOfAuthor($authorId: Int!) {
  postsOf(authorId: $authorId) {
    id
    title
  }
}

The variable $authorId is declared with its type (Int!) and passed as a separate JSON object when the query is executed.

Mutations use the same syntax but are write operations:

mutation CreatePost($title: String!, $content: String!) {
  createPost(title: $title, content: $content) {
    id
    title
  }
}

Mutations return data just like queries, and the client specifies which fields of the created or modified object it wants back.


c. Setting up Apollo Angular

Apollo Angular is the GraphQL client for Angular. It provides a service that executes queries and mutations, an in-memory cache that normalizes data by type and ID, and observables that update when the cache changes.

Installation uses the Angular CLI schematic:

ng add apollo-angular

This installs the required packages and configures the client. The GraphQL endpoint is set in the generated provider:

import { ApplicationConfig } from '@angular/core';
import { provideApollo } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache } from '@apollo/client/core';
import { inject } from '@angular/core';

export const graphqlProvider = provideApollo(() => {
  const httpLink = inject(HttpLink);
  return {
    link: httpLink.create({ uri: 'https://api.example.com/graphql' }),
    cache: new InMemoryCache(),
  };
});

The InMemoryCache caches query results by type and ID. When a mutation updates an object, Apollo can automatically update the cached object and notify all components watching it, so the UI stays in sync without manual refetching.


d. Querying with watchQuery

The Apollo service provides watchQuery for queries that should update when the cache changes and query for one-time fetches. watchQuery returns a QueryRef whose valueChanges property is an RxJS observable.

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Apollo, gql } from 'apollo-angular';
import { map } from 'rxjs';

const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      author { name }
    }
  }
`;

@Component({
  selector: 'app-posts',
  imports: [AsyncPipe],
  template: `
    @if (posts$ | async; as posts) {
      <ul>
        @for (post of posts; track post.id) {
          <li>{{ post.title }} by {{ post.author.name }}</li>
        }
      </ul>
    }
  `,
})
export class PostsComponent {
  private readonly apollo = inject(Apollo);

  posts$ = this.apollo.watchQuery<{ posts: Post[] }>({
    query: GET_POSTS,
  }).valueChanges.pipe(
    map(result => result.data.posts),
  );
}

The watchQuery observable emits once when the query completes and again whenever the cache entry for posts changes. The AsyncPipe subscribes and unsubscribes automatically, and @if with as avoids multiple async pipe subscriptions.

A key advantage of watchQuery over query is cache reactivity. If a mutation adds a post to the cache, any component watching posts receives the updated list without refetching.


e. Mutations and cache updates

Mutations are executed with the mutate method. After a mutation, the cache must be updated to reflect the change. Apollo provides two strategies: refetchQueries to re-execute queries after the mutation, and manual cache updates via update.

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
    }
  }
`;

createPost(title: string, content: string) {
  return this.apollo.mutate({
    mutation: CREATE_POST,
    variables: { title, content },
    refetchQueries: [{ query: GET_POSTS }],
  });
}

The refetchQueries option tells Apollo to re-execute the specified queries after the mutation completes, ensuring the UI reflects the new data. For more precise control, the update function receives the cache and the mutation result, and can write the new object directly into the cache.

Apollo’s normalized cache means that if the mutation returns the created post and the query for posts includes the same fields, the cache can automatically merge the new post into the existing list, and watchQuery emits the updated data.


f. Code generation for type safety

Manually maintaining TypeScript interfaces that match GraphQL queries is error-prone. GraphQL Code Generator with the @graphql-codegen/typescript-apollo-angular plugin generates typed Apollo services for each operation, eliminating manual interfaces and ensuring that queries and types stay in sync with the schema.

Installation:

npm i graphql
npm i -D @graphql-codegen/cli
npm i -D @graphql-codegen/typescript
npm i -D @graphql-codegen/typescript-operations
npm i -D @graphql-codegen/typescript-apollo-angular

Configuration in codegen.ts:

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',
  documents: './src/**/*.ts',
  generates: {
    './src/graphql/generated.ts': {
      plugins: [
        'typescript',
        'typescript-operations',
        'typescript-apollo-angular',
      ],
    },
  },
};

export default config;

Running npm run generate produces a generated.ts file with typed services. The component then uses the generated service directly:

import { PostsGQL, PostsQuery } from './graphql/generated';

@Component({ /* ... */ })
export class PostsComponent {
  posts$: Observable<PostsQuery['posts']>;

  constructor(postsGQL: PostsGQL) {
    this.posts$ = postsGQL.watch().valueChanges.pipe(
      map(result => result.data.posts),
    );
  }
}

The generated PostsGQL service provides a watch() method that returns a QueryRef with fully typed data. The type PostsQuery['posts'] is derived from the query definition, so if the query changes, the type changes automatically.


Complete Example Session

// ============================================
// PART 1: INSTALL APOLLO ANGULAR
// ============================================
// ng add apollo-angular
// ============================================
// PART 2: CONFIGURE THE CLIENT
// ============================================
import { ApplicationConfig } from '@angular/core';
import { provideApollo } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache } from '@apollo/client/core';
import { inject } from '@angular/core';

export const graphqlProvider = provideApollo(() => {
  const httpLink = inject(HttpLink);
  return {
    link: httpLink.create({ uri: 'http://localhost:4000/graphql' }),
    cache: new InMemoryCache(),
  };
});
// ============================================
// PART 3: DEFINE A QUERY WITH gql
// ============================================
import { gql } from 'apollo-angular';

const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      author {
        name
      }
    }
  }
`;
// ============================================
// PART 4: FETCH WITH watchQuery
// ============================================
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Apollo } from 'apollo-angular';
import { map } from 'rxjs';

@Component({
  selector: 'app-posts',
  imports: [AsyncPipe],
  template: `
    @if (posts$ | async; as posts) {
      <ul>
        @for (post of posts; track post.id) {
          <li>{{ post.title }} by {{ post.author.name }}</li>
        }
      </ul>
    }
  `,
})
export class PostsComponent {
  private readonly apollo = inject(Apollo);

  posts$ = this.apollo.watchQuery<{ posts: Post[] }>({
    query: GET_POSTS,
  }).valueChanges.pipe(
    map(result => result.data.posts),
  );
}
// ============================================
// PART 5: DEFINE A MUTATION
// ============================================
const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
    }
  }
`;
// ============================================
// PART 6: EXECUTE A MUTATION
// ============================================
createPost(title: string, content: string) {
  return this.apollo.mutate({
    mutation: CREATE_POST,
    variables: { title, content },
    refetchQueries: [{ query: GET_POSTS }],
  });
}
// ============================================
// PART 7: USING VARIABLES
// ============================================
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

getUser(id: string) {
  return this.apollo.watchQuery({
    query: GET_USER,
    variables: { id },
  }).valueChanges;
}
// ============================================
// PART 8: CODE GENERATION CONFIGURATION
// ============================================
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',
  documents: './src/**/*.ts',
  generates: {
    './src/graphql/generated.ts': {
      plugins: ['typescript', 'typescript-operations', 'typescript-apollo-angular'],
    },
  },
};

export default config;
// ============================================
// PART 9: USING GENERATED SERVICES
// ============================================
import { PostsGQL } from './graphql/generated';

@Component({ /* ... */ })
export class PostsComponent {
  posts$: Observable<PostsQuery['posts']>;

  constructor(postsGQL: PostsGQL) {
    this.posts$ = postsGQL.watch().valueChanges.pipe(
      map(result => result.data.posts),
    );
  }
}
// ============================================
// PART 10: CACHE UPDATE AFTER MUTATION
// ============================================
this.apollo.mutate({
  mutation: CREATE_POST,
  variables: { title, content },
  update: (cache, { data }) => {
    const newPost = data?.createPost;
    if (!newPost) return;

    const existing = cache.readQuery<{ posts: Post[] }>({ query: GET_POSTS });
    if (existing) {
      cache.writeQuery({
        query: GET_POSTS,
        data: { posts: [...existing.posts, newPost] },
      });
    }
  },
});

These ten parts cover installation, client configuration, query definition, watchQuery usage, mutation definition, mutation execution, variables, code generation configuration, using generated services, and manual cache updates.


Quick Reference

GraphQL vs REST

AspectRESTGraphQL
EndpointsMultipleSingle
Response shapeServer-definedClient-specified
Over-fetchingCommonEliminated
Under-fetchingRequires multiple callsSingle query
VersioningURL versionsSchema evolution
ToolingLimited introspectionFull introspection

Apollo Angular API

MethodPurposeReturns
watchQueryReactive queryQueryRef with valueChanges
queryOne-time queryObservable
mutateWrite operationObservable
subscribeReal-time subscriptionObservable

gql Tag

UsageExample
Querygql\query GetPosts { posts { id } }“
Mutationgql\mutation CreatePost($t: String!) { … }“
Fragmentgql\fragment PostFields on Post { id title }“

Code Generation

PackagePurpose
@graphql-codegen/cliCLI
@graphql-codegen/typescriptBase types
@graphql-codegen/typescript-operationsOperation types
@graphql-codegen/typescript-apollo-angularTyped Angular services

Best Practices

✅ Do This:

// Use watchQuery for reactive data
posts$ = this.apollo.watchQuery({ query: GET_POSTS }).valueChanges;

// Use variables for reusable queries
const GET_USER = gql`query GetUser($id: ID!) { user(id: $id) { name } }`;

// Generate types from the schema
npm run generate

// Update cache after mutations
refetchQueries: [{ query: GET_POSTS }]

❌ Don’t Do This:

// Don't use query() when data should be reactive
this.apollo.query({ query: GET_POSTS });  // ❌ one-time only

// Don't hardcode values in queries
gql`{ user(id: "123") { name } }`  // ❌ use variables

// Don't manually maintain types
interface Post { id: string; title: string; }  // ❌ use codegen

Common Pitfalls

PitfallWhy It HappensFix
Stale data after mutationCache not updatedUse refetchQueries or update
Types out of syncManual interfacesUse GraphQL Code Generator
Query not reactiveUsed query instead of watchQueryUse watchQuery
Variables not passedMissing variables optionPass variables in the options object
Multiple async pipesMultiple subscriptionsUse @if (x$ | async; as x)
Subscription leakNot unsubscribingUse AsyncPipe or takeUntilDestroyed

Real-World Examples

1. Basic Query

const GET_POSTS = gql`{ posts { id title } }`;

2. Query with Variables

const GET_USER = gql`query GetUser($id: ID!) { user(id: $id) { name } }`;

3. Mutation

const CREATE_POST = gql`
  mutation CreatePost($title: String!) {
    createPost(title: $title) { id title }
  }
`;

4. Reactive Query in Component

posts$ = this.apollo.watchQuery({ query: GET_POSTS }).valueChanges;

5. Mutation with Refetch

this.apollo.mutate({
  mutation: CREATE_POST,
  refetchQueries: [{ query: GET_POSTS }],
});

6. Manual Cache Update

update: (cache, { data }) => {
  cache.writeQuery({ query: GET_POSTS, data: { posts: [...] } });
}

7. Generated Service

import { PostsGQL } from './graphql/generated';
constructor(postsGQL: PostsGQL) { ... }

8. Fragment

const POST_FIELDS = gql`
  fragment PostFields on Post { id title author { name } }
`;

9. Error Handling

this.apollo.watchQuery({ query: GET_POSTS }).valueChanges.subscribe({
  next: ({ data }) => { ... },
  error: (err) => { ... },
});

10. Subscription

this.apollo.subscribe({ query: POST_ADDED }).subscribe({
  next: ({ data }) => { ... },
});

Visual

GraphQL vs REST Request Flow

┌──────────────────────────────────────────────────────────────┐
│  REST                                    GraphQL              │
│                                                              │
│  GET /users/1          ──▶  { id, name, email, ... }         │
│  GET /users/1/posts    ──▶  [{ id, title }, ...]             │
│                                                              │
│  Two requests.                        One request.            │
│  Over-fetching.                       Exact fields.           │
│  Under-fetching.                      Related data.           │
└──────────────────────────────────────────────────────────────┘

Apollo Cache Reactivity

┌──────────────────────────────────────────────────────────────┐
│  MUTATION                    CACHE                  UI        │
│  ┌─────────────┐             ┌─────────┐         ┌─────────┐  │
│  │ createPost  │────────────▶│  posts  │────────▶│ watchQuery│ │
│  └─────────────┘             │  [new]  │         │  emits  │  │
│                              └─────────┘         └─────────┘  │
│                                                              │
│  Mutation writes to cache; watchQuery subscribers receive    │
│  the updated data without refetching.                        │
└──────────────────────────────────────────────────────────────┘

Code Generation Flow

┌──────────────────────────────────────────────────────────────┐
│  schema.graphql          queries.ts            generated.ts  │
│       │                       │                      │       │
│       └───────────┬───────────┘                      │       │
│                   ▼                                  │       │
│            graphql-codegen                           │       │
│                   │                                  │       │
│                   ▼                                  │       │
│            generated.ts ─────────────────────────────▶│       │
│                                                              │
│  Typed services derived from schema and queries.             │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
GraphQLQuery language for APIs with a typed schema
Single endpointAll operations sent to one URL
Over-fetchingEliminated; client specifies fields
Under-fetchingEliminated; single query for related data
SchemaType system defining available operations
QueryRead operation
MutationWrite operation
SubscriptionReal-time push operation
Apollo AngularGraphQL client for Angular
watchQueryReactive query returning observable
CacheNormalized by type and ID
Code generation@graphql-codegen/typescript-apollo-angular

Key takeaways:

  • GraphQL is a query language and runtime, not a database technology. It sits in front of existing data sources—databases, microservices, REST APIs—and provides a unified, typed interface for clients.
  • The client specifies the response shape. Unlike REST, where the server decides what a resource returns, GraphQL queries declare exactly which fields the client needs, eliminating over-fetching and under-fetching.
  • The schema is the contract. Written in SDL, the schema defines types, queries, mutations, and subscriptions. It is introspectable, enabling tooling and self-documentation.
  • Apollo Angular provides watchQuery for reactive data. The valueChanges observable emits when the query completes and whenever the cache entry changes, keeping the UI in sync automatically.
  • Mutations return data like queries. The client specifies which fields of the modified object it wants back, and can use refetchQueries or update to keep the cache consistent.
  • Code generation eliminates manual types. GraphQL Code Generator with the Apollo Angular plugin produces typed services from the schema and query documents, ensuring that types stay synchronized with the API.
  • Apollo’s normalized cache is the reactivity mechanism. Objects are cached by __typename:id, so a mutation that updates an object notifies every component watching it, without manual refetching.

Remember: GraphQL in Angular is not a replacement for REST in every case; it is a different contract between client and server. The server publishes a schema describing what can be queried, and the client sends a query specifying exactly what it needs. Apollo Angular implements this contract with an observable-based API and a normalized cache that makes data reactive. The watchQuery method is the core primitive: it executes a query and returns an observable that updates when the cache changes. Mutations write to the cache, and code generation keeps types aligned with the schema. The result is an application where data fetching is declarative, types are generated rather than manually maintained, and the UI stays synchronized with the server state without explicit refetching logic.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!