ledgerhq/ledger-live · Archived

rtk-query-api

RTK Query createApi best practices

First seen Jun 19, 2026

Installation

$ npx skills add ledgerhq/ledger-live --skill rtk-query-api

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from ledgerhq/ledger-live.

npx skills add ledgerhq/ledger-live

Browse all from ledgerhq/ledger-live

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 604
License LICENSE.txt
Default branch develop
Open issues 7
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 6,422 B
  • docs SUMMARY.md 55 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

RTK Query - createApi

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:

Half Owner Contains
Reaching a backend [@shared/api-services](../../../shared/api-services/README.md) — 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. 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.