Skip to main content

Overview

Code splitting allows you to lazy load API definitions in feature modules, reducing initial bundle size and improving application performance. ngrx-rtk-query supports multiple patterns for code splitting.

When to Use Code Splitting

Code splitting is beneficial when:
  • You have multiple feature modules with different API endpoints
  • Your application has distinct user flows (e.g., admin, user, public)
  • You want to reduce initial bundle size
  • Features are used independently or conditionally
For APIs sharing the same base URL, prefer RTK Query’s code splitting with injectEndpoints() instead.

Basic Code Splitting

Lazy load API definitions with Angular’s route-based code splitting:
1

Create feature API

features/posts/api.ts
import { createApi, fetchBaseQuery } from 'ngrx-rtk-query';

export interface Post {
  id: number;
  name: string;
  content: string;
}

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://posts.api.com' }),
  tagTypes: ['Posts'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => '/posts',
      providesTags: ['Posts'],
    }),
    addPost: build.mutation<Post, Partial<Post>>({
      query: (body) => ({
        url: '/posts',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['Posts'],
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = postsApi;
2

Provide API in lazy route

features/posts/routes.ts
import { Route } from '@angular/router';
import { provideStoreApi } from 'ngrx-rtk-query';
import { postsApi } from './api';

export const POSTS_ROUTES: Route[] = [
  {
    path: '',
    providers: [provideStoreApi(postsApi)],
    children: [
      {
        path: '',
        loadComponent: () => import('./posts-list.component')
          .then((c) => c.PostsListComponent),
      },
      {
        path: ':id',
        loadComponent: () => import('./post-details.component')
          .then((c) => c.PostDetailsComponent),
      },
    ],
  },
];
3

Lazy load in app routes

app.routes.ts
import { Route } from '@angular/router';

export const appRoutes: Route[] = [
  {
    path: 'posts',
    loadChildren: () => import('./features/posts/routes')
      .then((r) => r.POSTS_ROUTES),
  },
  {
    path: 'users',
    loadChildren: () => import('./features/users/routes')
      .then((r) => r.USERS_ROUTES),
  },
];
The API is only loaded when the user navigates to the /posts route.

Multiple APIs with Different Base URLs

When you have multiple APIs with different base URLs, provide each API in its feature route:
features/posts/api.ts
export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({ 
    baseUrl: 'https://posts-api.example.com' 
  }),
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => '/posts',
    }),
  }),
});
Each API must have a unique reducerPath to avoid conflicts.

Same Base URL with injectEndpoints

For APIs sharing the same base URL, use RTK Query’s injectEndpoints() pattern:
1

Create base API

core/api.ts
import { createApi, fetchBaseQuery } from 'ngrx-rtk-query';

// Create base API with empty endpoints
export const baseApi = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
  tagTypes: ['Posts', 'Users', 'Comments'],
  endpoints: () => ({}),
});
2

Inject endpoints in features

features/posts/api.ts
import { baseApi } from '@/core/api';

export const postsApi = baseApi.injectEndpoints({
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => '/posts',
      providesTags: ['Posts'],
    }),
    addPost: build.mutation<Post, Partial<Post>>({
      query: (body) => ({
        url: '/posts',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['Posts'],
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = postsApi;
3

Provide base API once

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideStoreApi } from 'ngrx-rtk-query';
import { baseApi } from './core/api';

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore(),
    provideStoreApi(baseApi), // Only provide base API
  ],
};
4

Use in lazy-loaded components

features/posts/posts-list.component.ts
import { Component } from '@angular/core';
import { useGetPostsQuery } from './api';

@Component({
  template: `
    @for (post of postsQuery.data(); track post.id) {
      <div>{{ post.name }}</div>
    }
  `,
})
export class PostsListComponent {
  // Hook is lazy-loaded with component
  postsQuery = useGetPostsQuery();
}
With injectEndpoints(), the base API reducer is loaded upfront, but endpoint implementations are lazy-loaded per feature.

Eager vs Lazy Loading

Load API immediately at app startup:
app.config.ts
import { provideStoreApi } from 'ngrx-rtk-query';
import { postsApi } from './posts/api';

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore(),
    provideStoreApi(postsApi), // Loaded immediately
  ],
};
Use when:
  • API is used across the entire app
  • Initial bundle size is not a concern
  • API must be available before first render

Preloading Strategies

Combine lazy loading with Angular’s preloading strategies:
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, PreloadAllModules } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      appRoutes,
      // Preload all lazy routes after initial render
      withPreloading(PreloadAllModules)
    ),
  ],
};

Custom Preload Strategy

Create a selective preloading strategy:
preload-strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data?.['preload']) {
      // Delay preload by 2 seconds
      return timer(2000).pipe(mergeMap(() => load()));
    }
    return of(null);
  }
}
app.routes.ts
export const appRoutes: Route[] = [
  {
    path: 'posts',
    data: { preload: true }, // Enable preloading
    loadChildren: () => import('./features/posts/routes')
      .then((r) => r.POSTS_ROUTES),
  },
];

Shared APIs Across Features

Share API instances across multiple features:
1

Create shared API

shared/api.ts
export const sharedApi = createApi({
  reducerPath: 'sharedApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
  tagTypes: ['Shared'],
  endpoints: (build) => ({
    getConfig: build.query<Config, void>({
      query: () => '/config',
    }),
  }),
});

export const { useGetConfigQuery } = sharedApi;
2

Provide in root

app.config.ts
import { provideStoreApi } from 'ngrx-rtk-query';
import { sharedApi } from './shared/api';

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore(),
    provideStoreApi(sharedApi), // Available everywhere
  ],
};
3

Use in any feature

features/posts/posts-list.component.ts
import { useGetConfigQuery } from '@/shared/api';
import { useGetPostsQuery } from './api';

export class PostsListComponent {
  configQuery = useGetConfigQuery(); // Shared API
  postsQuery = useGetPostsQuery();   // Feature API
}

Bundle Analysis

Analyze your bundle to verify code splitting:
# Build with stats
ng build --stats-json

# Analyze with webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/stats.json
Look for:
  • API definitions in separate chunks
  • Reduced main bundle size
  • Lazy-loaded feature chunks

Performance Considerations

Reduced Initial Bundle Size
  • Faster initial page load
  • Improved Time to Interactive (TTI)
  • Better Lighthouse scores
On-Demand Loading
  • Load only what’s needed
  • Smaller cache footprint
  • Faster subsequent navigations
Better Caching
  • Unchanged features remain cached
  • More granular cache invalidation

Best Practices

1

Use injectEndpoints for shared base URLs

Don’t create multiple APIs for the same backend:
// ✅ Good - single API, injected endpoints
const baseApi = createApi({ reducerPath: 'api', ... });
const postsApi = baseApi.injectEndpoints({ ... });

// ❌ Bad - multiple APIs, same base URL
const postsApi = createApi({ baseUrl: 'https://api.com', ... });
const usersApi = createApi({ baseUrl: 'https://api.com', ... });
2

Provide APIs at the correct level

  • Root-level: Shared across entire app
  • Feature route: Used only in that feature
  • Component: Very rare, usually not recommended
3

Use unique reducerPath for each API

const postsApi = createApi({ reducerPath: 'postsApi', ... });
const usersApi = createApi({ reducerPath: 'usersApi', ... });
4

Consider preloading strategies

Balance initial load time with user experience:
  • Critical features: Eager load
  • Common features: Preload
  • Rare features: Lazy load
5

Monitor bundle sizes

Regularly analyze bundles to ensure code splitting is effective.

Troubleshooting

API not available in lazy route

Ensure provideStoreApi() is in the route’s providers array.
// ✅ Correct
{
  path: 'posts',
  providers: [provideStoreApi(postsApi)],
  loadChildren: () => import('./posts/routes'),
}

// ❌ Wrong - missing providers
{
  path: 'posts',
  loadChildren: () => import('./posts/routes'),
}

Duplicate API registrations

If you see errors about duplicate reducerPath:
  1. Check that each API has a unique reducerPath
  2. Ensure you’re not providing the same API multiple times
  3. Use injectEndpoints() for shared base URLs

Slow initial route navigation

If lazy routes load slowly:
  1. Use a preloading strategy
  2. Show loading indicators during route transitions
  3. Consider eager loading frequently-used features

Build docs developers (and LLMs) love