RTK Query - createApi
SkillDev toolsRTK Query createApi best practices
Available today. Use it from your connected AI after setup.
No other account needed.
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
createApicalls 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:
| Half | Owner | Contains |
|---|---|---|
| Reaching a backend | @shared/api-services — one dir per backend | Base 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.
injectEndpointsdoes not accepttagTypes, 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 —
injectEndpointscannot 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-servicesin 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. overrideExistingdefaults tofalse— injecting an endpoint name that already exists is silently ignored unless you opt in.
Endpoints
- Use
build.queryfor GET requests - Use
build.mutationfor POST/PUT/DELETE - Type both response and argument:
build.query<ResponseType, ArgType> - Use
voidfor no arguments:build.query<Data[], void>
Caching & Tags
- Define tags as enums in
types.ts - Use
providesTagson queries for cache invalidation - Use
invalidatesTagson mutations to trigger refetch - Use
keepUnusedDataForfor 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
transformResponseto reshape API data - Use
transformErrorResponsefor custom error handling
getItems: build.query<Item[], void>({
query: () => "items",
transformResponse: (response: ApiResponse) => response.data.items,
}),
Error Handling
- Always catch errors in custom
baseQueryorqueryFn - 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