RTK Query - createApi

SkillDev tools

RTK Query createApi best practices

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the RTK Query - createApi skill

What this skill tells your AI

The instructions your AI receives, as published by ledgerhq/ledger-live in .agents/skills/rtk-query-api/SKILL.md and read by ahel’s review.

Structure

  • One API slice per base URL / data source — never two createApi calls against the same backend
  • Export generated hooks alongside the API
// ✅ GOOD - state-manager/api.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { EntityTags } from "./types";

export const myApi = createApi({
  reducerPath: "myApi",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  tagTypes: [EntityTags.Entity, EntityTags.Entities],
  endpoints: (build) => ({
    getEntity: build.query<Entity, string>({
      query: (id) => `entities/${id}`,
      providesTags: [EntityTags.Entity],
    }),
  }),
});

export const { useGetEntityQuery } = myApi;

Define tags as enums in state-manager/types.ts:

export enum EntityTags {
  Entity = "Entity",
  Entities = "Entities",
}

Splitting backend access from use case

In domain/api/, this is the default — not something you reach for once a second use case appears. Always split reaching the backend from what you ask it for:

HalfOwnerContains
Reaching a backend@shared/api-services — one dir per backendBase URL, base query, retry, reducerPath, extraArgument contract
What you ask it for@domain/api-<name>Endpoints, wire schemas, transforms, cache tags, hooks

Doing it upfront costs nothing and means the second use case is a one-line addition rather than a migration. Two createApi calls against one backend would give you two store slices, two caches and two middlewares for one service.

The shared half declares an empty api. The use-case half adds to it with injectEndpoints for endpoints and enhanceEndpoints({ addTagTypes }) for tags. Both mutate and return the same api object, so one reducer, one middleware and one cache serve every use case.

There are no exceptions. If a backend's base query currently needs use-case knowledge — mock handlers keyed by endpoint URL, endpoint-name lookups, response types from its own wire schemas — that is a problem to fix in the base query, not a reason to keep a second createApi.

// ✅ GOOD - the service api: base query + config. No endpoints, no tags.
export const myServiceApi = createApi({
  reducerPath: "myServiceApi",
  baseQuery: myServiceBaseQuery,
  tagTypes: [],
  endpoints: () => ({}),
});
// ✅ GOOD - a use case adds its own tags, then its endpoints
export const FIRST_USE_CASE_TAGS = ["Entity"] as const;

export const firstUseCaseApi = myServiceApi
  .enhanceEndpoints({ addTagTypes: FIRST_USE_CASE_TAGS })
  .injectEndpoints({
    endpoints: build => ({
      getEntity: build.query<Entity, string>({
        query: id => `entities/${id}`,
        providesTags: [...FIRST_USE_CASE_TAGS],
      }),
    }),
  });

export const { useGetEntityQuery } = firstUseCaseApi;
  • Cache tags belong to the use case, not the shared api. injectEndpoints does not accept tagTypes, which makes it tempting to declare every tag upfront in the shared file — don't. enhanceEndpoints({ addTagTypes }) widens the tag union in place, so a tag stays next to the endpoints that provide it and adding a use case never means editing a shared file.
  • Register the service api; call endpoints on the use case. Only the injected reference is typed with the endpoints — injectEndpoints cannot retype the original.
  • Injection is a module-level side effect. An endpoint exists only once its use-case module has been evaluated as a value import; a type-only import will not trigger it. Never import an api from @shared/api-services in order to call endpoints on it.
  • A tag-less api has a narrower state type. The registered api declares no tags, so a helper typed on an injected reference (whose use case added some) will not accept an app's State. Type such helpers on the service api.
  • overrideExisting defaults to false — injecting an endpoint name that already exists is silently ignored unless you opt in.

Endpoints

  • Use build.query for GET requests
  • Use build.mutation for POST/PUT/DELETE
  • Type both response and argument: build.query<ResponseType, ArgType>
  • Use void for no arguments: build.query<Data[], void>

Caching & Tags

  • Define tags as enums in types.ts
  • Use providesTags on queries for cache invalidation
  • Use invalidatesTags on mutations to trigger refetch
  • Use keepUnusedDataFor for custom cache duration
endpoints: (build) => ({
  getItems: build.query<Item[], void>({
    query: () => "items",
    providesTags: [ItemTags.Items],
    keepUnusedDataFor: 60, // seconds
  }),
  addItem: build.mutation<Item, Partial<Item>>({
    query: (body) => ({ url: "items", method: "POST", body }),
    invalidatesTags: [ItemTags.Items],
  }),
}),

Transform Responses

  • Use transformResponse to reshape API data
  • Use transformErrorResponse for custom error handling
getItems: build.query<Item[], void>({
  query: () => "items",
  transformResponse: (response: ApiResponse) => response.data.items,
}),

Error Handling

  • Always catch errors in custom baseQuery or queryFn
  • Return { data } on success, { error } on failure
// ✅ GOOD - errors are caught and returned
queryFn: async (arg) => {
  try {
    const data = await fetchData(arg);
    return { data };
  } catch (error) {
    return { error: { status: "CUSTOM_ERROR", data: error } };
  }
},

Registration

Register APIs in reducers/rtkQueryApi.ts, keyed by reducerPath. For a shared backend, register the service api — its endpoints arrive via the use-case packages the view-models import. The registry then reads as a list of the backends the app talks to:

const APIs = {
  [myApi.reducerPath]: myApi,
  [myServiceApi.reducerPath]: myServiceApi,
};

Two entries whose reducerPath resolves to the same string is a compile error (TS1117: An object literal cannot have multiple properties with the same name), even for computed properties — which is what catches an accidental double-registration of one backend.

Signals

GitHub stars
618
Forks
490
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
rtk-query-api
Source
github.com/ledgerhq/ledger-live