Skip to main content

Quick Start

This guide will walk you through creating your first API integration with ngrx-rtk-query, from defining endpoints to using them in your components.

Overview

We’ll build a simple posts management feature that demonstrates:
  • Creating an API with queries and mutations
  • Configuring the store provider
  • Using queries in components
  • Triggering mutations
  • Working with signal inputs

Step-by-Step Guide

1

Define Your Data Model

Start by creating TypeScript interfaces for your data:
post.model.ts
export interface Post {
  id: number;
  name: string;
  fetched_at: string;
}
2

Create Your API Definition

Use createApi to define your API endpoints. This will auto-generate typed hooks:
api.ts
import { createApi, fetchBaseQuery } from 'ngrx-rtk-query';
import { type Post } from './post.model';

export const postsApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'http://api.localhost.com' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<Post[], void>({
  query: () => ({ url: '/posts' }),
  providesTags: (result) =>
    result
      ? [...result.map(({ id }) => ({ type: 'Posts', id }) as const), { type: 'Posts', id: 'LIST' }]
      : [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query<Post, number>({
  query: (id) => `/posts/${id}`,
  providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
addPost: build.mutation<Post, Partial<Post>>({
  query: (body) => ({
    url: `/posts`,
    method: 'POST',
    body,
  }),
  invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
updatePost: build.mutation<Post, Partial<Post>>({
  query: (data) => {
    const { id, ...body } = data;
    return {
      url: `/posts/${id}`,
      method: 'PUT',
      body,
    };
  },
  invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
}),
deletePost: build.mutation<{ success: boolean; id: number }, number>({
  query: (id) => ({
    url: `/posts/${id}`,
    method: 'DELETE',
  }),
  invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
}),
});

// Export auto-generated hooks
export const {
useGetPostsQuery,
useGetPostQuery,
useAddPostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} = postsApi;
The tagTypes and cache tags (providesTags/invalidatesTags) enable automatic cache invalidation when data changes.
3

Configure Your Application

Add the API to your application providers:
import { type ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { provideStoreApi } from 'ngrx-rtk-query';

import { appRoutes } from './app.routes';
import { postsApi } from './posts/api';

export const appConfig: ApplicationConfig = {
providers: [
provideStore(),
provideStoreDevtools({
  name: 'RTK Query App',
  logOnly: !isDevMode(),
}),
provideStoreApi(postsApi),
provideRouter(appRoutes),
],
};
You can disable setupListeners if needed: provideStoreApi(postsApi, { setupListeners: false })
4

Use Queries in a Component

Create a list component that fetches and displays posts:
posts-list.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { useAddPostMutation, useGetPostsQuery } from './api';

@Component({
selector: 'app-posts-list',
standalone: true,
imports: [FormsModule, RouterLink],
template: `
<section>
  <input
    type="text"
    placeholder="New post name"
    [ngModel]="postNameControl()"
    (ngModelChange)="postNameControl.set($event)"
  />
  <button
    [disabled]="!postNameControl() || addPost.isLoading()"
    (click)="addNewPost()"
  >
    {{ addPost.isLoading() ? 'Adding...' : 'Add Post' }}
  </button>
</section>

<section>
  <h1>Posts List</h1>
  @if (postsQuery.isLoading()) {
    <small>Loading...</small>
  }
  @if (postsQuery.isError()) {
    <small>Error...</small>
  }
  @if (postsQuery.data(); as posts) {
    @for (post of posts; track post.id) {
      <li>
        <a [routerLink]="[post.id]">{{ post.name }}</a>
      </li>
    } @empty {
      <p>No posts :(</p>
    }
  }
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PostsListComponent {
readonly postsQuery = useGetPostsQuery();
readonly addPost = useAddPostMutation();
readonly postNameControl = signal('');

addNewPost() {
this.addPost({ name: this.postNameControl() })
  .unwrap()
  .then(() => {
    this.postNameControl.set('');
  });
}
}
The query hook returns a signal with properties like isLoading(), isError(), data(), etc. You can access them as postsQuery.isLoading() for fine-grained change detection.
5

Use Dynamic Query Arguments

Create a detail component that uses signal inputs:
post-details.component.ts
import { ChangeDetectionStrategy, Component, input, numberAttribute, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
import { useGetPostQuery, useUpdatePostMutation, useDeletePostMutation } from './api';

@Component({
selector: 'app-post-details',
standalone: true,
imports: [FormsModule, JsonPipe],
template: `
<h1>{{ postQuery.data()?.name }}</h1>

@if (!isEditing()) {
  <button
    [disabled]="postQuery.isLoading() || updatePostMutation.isLoading()"
    (click)="toggleEdit()"
  >
    Edit
  </button>
  <button
    [disabled]="postQuery.isFetching()"
    (click)="postQuery.refetch()"
  >
    {{ postQuery.isFetching() ? 'Fetching...' : 'Refresh' }}
  </button>
} @else {
  <input
    type="text"
    [ngModel]="postNameControl()"
    (ngModelChange)="postNameControl.set($event)"
  />
  <button
    [disabled]="updatePostMutation.isLoading()"
    (click)="updatePost()"
  >
    {{ updatePostMutation.isLoading() ? 'Updating...' : 'Update' }}
  </button>
  <button (click)="toggleEdit()">Cancel</button>
}

@if (postQuery.isFetching()) {
  <p>Loading...</p>
}
<pre>{{ postQuery.data() | json }}</pre>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PostDetailsComponent {
// Signal input from route parameter
readonly id = input.required({ transform: numberAttribute });

// Query with signal input and polling
readonly postQuery = useGetPostQuery(this.id, {
pollingInterval: 5000,
});

readonly updatePostMutation = useUpdatePostMutation();
readonly deletePostMutation = useDeletePostMutation();

readonly postNameControl = signal('');
readonly isEditing = signal(false);

updatePost(): void {
this.updatePostMutation({ id: this.id(), name: this.postNameControl() })
  .unwrap()
  .then(() => this.toggleEdit());
}

toggleEdit(): void {
this.isEditing.update((isEditing) => !isEditing);
}
}
Pass signal inputs directly to query hooks - they’ll automatically react to changes! You can also pass functions for computed values: useGetPostQuery(() => this.userId())

Key Concepts

Auto-Generated Hooks

When you export hooks from your API, they’re fully typed and ready to use:
const postsQuery = useGetPostsQuery(); // Returns Signal with data, isLoading, etc.
const addPost = useAddPostMutation();   // Returns mutation trigger + state

Signal Access Patterns

You can access query/mutation state in two ways:
// Fine-grained (recommended for better change detection)
postsQuery.isLoading()  // Direct signal access
postsQuery.data()       // Direct signal access

// Object access (also works)
postsQuery().isLoading  // Access via main signal
postsQuery().data       // Access via main signal

Cache Invalidation

Use tags to automatically invalidate and refetch data:
  • providesTags: Tags that this endpoint provides
  • invalidatesTags: Tags to invalidate when mutation succeeds
getPosts: build.query({
  providesTags: [{ type: 'Posts', id: 'LIST' }],
}),
addPost: build.mutation({
  invalidatesTags: [{ type: 'Posts', id: 'LIST' }], // Refetches getPosts
}),

Mutation Unwrapping

Use .unwrap() to handle success/error:
this.addPost({ name: 'New Post' })
  .unwrap()
  .then((data) => {
    // Handle success
  })
  .catch((error) => {
    // Handle error
  });

// Or with async/await
try {
  const data = await this.addPost({ name: 'New Post' }).unwrap();
  // Handle success
} catch (error) {
  // Handle error
}

What’s Next?

Now that you’ve built your first API integration, explore more advanced features:

Queries

Learn about lazy queries, skip tokens, and query options

Mutations

Master mutations, optimistic updates, and error handling

Infinite Queries

Implement pagination and infinite scrolling

Code Splitting

Learn how to lazy-load API definitions
You can follow the official RTK Query guide with slight variations for Angular-specific patterns.

Build docs developers (and LLMs) love