Solid Entity API
Overview
This generic Solid Entity API factory (createSolidEntityApi) provides a single, consistent surface for common CRUD operations across "entities" in SolidX applications.
Goals:
- Keep API layer DRY by generating endpoints for common patterns (find many, find one, create, update, delete, recover, patch).
- Standardize caching and invalidation using RTK Query
providesTags/invalidatesTags. - Allow flexible server-side filtering via query-string filters while keeping a simple client-side hook surface.
- Make it easy to plug into frontends (React + RTK Query) with clear patterns for eager vs lazy fetching, conditional fetch, and refetch control.
Factory overview (reference)
export const createSolidEntityApi = (entityName: string) => {
const kebabEntityName = kebabCase(entityName);
return createApi({
reducerPath: `genericSolid${entityName}Api`,
baseQuery: baseQueryWithAuth,
tagTypes: [entityName],
endpoints: (builder) => ({
getSolidEntities: builder.query({...}),
getSolidEntityById: builder.query({...}),
createSolidEntity: builder.mutation({...}),
updateSolidEntity: builder.mutation({...}),
patchUpdateSolidEntity: builder.mutation({...}),
deleteSolidEntity: builder.mutation({...}),
deleteMultipleSolidEntities: builder.mutation({...}),
recoverSolidEntity: builder.mutation({...}),
recoverSolidEntityById: builder.query({...}),
}),
});
};
The actual implementation used in the project defines
providesTagsfor read endpoints andinvalidatesTagsfor mutations to ensure automatic refetches of affected queries.
Endpoints and semantics
getSolidEntities — find many
- Method: GET
- Path:
/${kebabEntityName}?${qs} - Returns:
{ records: any[], meta: object, groupMeta?: any, groupRecords?: any[] } - Provides tags:
[{ type: entityName, id: record.id }, { type: entityName, id: 'LIST' }]for each record and a LIST tag.
Use-cases:
- Listing, table/grid data.
- Paginated requests using
offsetandlimit. - Complex server-side filtering using
filters[...]query string structure. - Sorting, selecting fields, populating relations, grouping.
Important: caching is keyed by the query string argument — identical qs (string) → same cache entry.
getSolidEntityById — find one
- Method: GET
- Path:
/${kebabEntityName}/${id}?${qs} - Provides tags:
{ type: entityName, id }
Use-cases:
- Detail view for a single resource.
- Fetch by id with optional
qsto control fields/populates.
Create / Update / Patch / Delete / Recover
Mutations are implemented with appropriate HTTP methods and invalidatesTags so that after a successful mutation, relevant cached queries are invalidated and refetched automatically.
createSolidEntity—POST /${kebabEntityName}→invalidatesTags: [{ type: entityName, id: 'LIST' }]updateSolidEntity—PUT /${kebabEntityName}/${id}→ invalidates the record id andLISTpatchUpdateSolidEntity—PATCH /${kebabEntityName}/${id}→ invalidates the record id andLISTdeleteSolidEntity—DELETE /${kebabEntityName}/${id}→ invalidates the record id andLISTdeleteMultipleSolidEntities—DELETE /${kebabEntityName}/bulk/→ invalidatesLISTrecoverSolidEntity—POST /${kebabEntityName}/bulk-recover/→ invalidatesLISTrecoverSolidEntityById—GET /${kebabEntityName}/recover/${id}→ provides tag for id
How caching and invalidation work (quick primer)
- RTK Query caches each query by its argument (here typically the
qsstring or{ id, qs }). providesTagstells RTK Query what tags the data corresponds to.invalidatesTagsin mutations tells RTK Query to mark those tags stale and auto-refetch related queries.- By default, when a cached query has no active subscribers, RTK Query keeps it for
keepUnusedDataForseconds (default: 60). You can override this globally or per-endpoint. - If a component remounts before the
keepUnusedDataFortimer expires, RTK Query reuses cached data instantly (no network request) unlessrefetchOnMountOrArgChangeis configured.
Building the query string
Recommended serialization:
import qs from 'qs';
const queryData = {
limit: 10,
offset: 0,
filters: { status: { $eq: 'active' } },
};
const queryString = qs.stringify(queryData, { encodeValuesOnly: true });
// pass queryString to the hook: useGetSolidEntitiesQuery(queryString);
Using
encodeValuesOnly: truekeeps keys readable (filters[name][$eq]=John) while properly encoding values.
Further References
- For comprehensive filtering syntax, see the Retrieve API Filters documentation.
Hook usage examples (React + RTK Query)
Assume const api = createSolidEntityApi('Person') and hooks are exported like:
const {
useGetSolidEntitiesQuery,
useLazyGetSolidEntitiesQuery,
useGetSolidEntityByIdQuery,
useLazyGetSolidEntityByIdQuery,
useCreateSolidEntityMutation,
useUpdateSolidEntityMutation,
// ...
} = api;
1) Find many — automatic fetch (eager)
function PersonList({ filtersObj }) {
const qs = useMemo(() => qs.stringify(filtersObj, { encodeValuesOnly: true }), [filtersObj]);
const { data, isLoading, isFetching, refetch } = useGetSolidEntitiesQuery(qs);
// data.records, data.meta
return <List ... />;
}
2) Find many — conditional (skip when filters not ready)
const { data } = useGetSolidEntitiesQuery(qs, { skip: !qs });
3) Find many — lazy (on-demand fetch, e.g., on search button)
const [trigger, { data, isFetching }] = useLazyGetSolidEntitiesQuery();
<button onClick={() => trigger(qs)}>Search</button>
4) Find one — eager
const { data } = useGetSolidEntityByIdQuery({ id, qs });
5) Find one — lazy
const [trigger, { data }] = useLazyGetSolidEntityByIdQuery();
<button onClick={() => trigger({ id, qs })}>Load</button>
6) Create / Update / Delete
const [createEntity] = useCreateSolidEntityMutation();
await createEntity(payload); // invalidates LIST -> causes getSolidEntities to refetch
const [updateEntity] = useUpdateSolidEntityMutation();
await updateEntity({ id, data }); // invalidates id and LIST
const [deleteEntity] = useDeleteSolidEntityMutation();
await deleteEntity(id); // invalidates id and LIST
When to use useGet vs useLazyGet
useGetXQuery (eager) — Use when:
- You want the data to be fetched automatically when the component mounts.
- The query args (e.g.,
qsorid) are available synchronously on mount. - You want built-in re-fetch strategies (on focus, reconnect, arg change).
useLazyGetXQuery (manual) — Use when:
- You need to fetch on demand (e.g., user presses "Search", or a form submits).
- You don’t have query args at mount time and don’t want to use
skip. - You want full programmatic control over when the request happens.
Important: never conditionally call hooks. Use skip option or lazy hooks to control whether the fetch runs.
Examples:
- If you only fetch after user input and want simple UX:
useLazyGetSolidEntitiesQuery()+trigger(qs). - If you want to mount and auto fetch when
idbecomes available, prefer:useGetSolidEntityByIdQuery({ id, qs }, { skip: !id });
Intricacies & best practices
- Stable serialization is crucial: Because RTK Query keys are based on the hook arguments, different string ordering produces different cache entries. Use
qs.stringifywith deterministic options. - Prefer the
LISTtag for collection invalidation: Your factory tags the list withid: 'LIST'so writes that change the list can invalidate and refetch the collection view. - Invalidate both id + LIST on update/delete: This removes stale record pages and refreshes lists that may be affected.
keepUnusedDataForbehavior: If components unmount and remount withinkeepUnusedDataForwindow, cached data is reused. Change the value if you need shorter/longer retention.- Refetch behavior on mount:
refetchOnMountOrArgChange: 'always' | true | falsecontrols whether cached data is revalidated on remount. Default isfalse(no refetch). - Paged requests: Ensure pagination params (
offset,limit) are part of theqsargument so different pages are cached separately. - Soft-deleted data: Use
showSoftDeletedoption to include soft-deleted records when needed.
Examples: Typical flows
Create -> Auto refresh list
- User opens list page =>
useGetSolidEntitiesQuery(qs)runs and shows cached or fetched data. - User creates a new entity using
createSolidEntitymutation. invalidatesTags: [{ type: 'Person', id: 'LIST' }]triggers a refetch ofgetSolidEntities.
Update detail -> Auto refresh detail & list
- User opens detail page =>
useGetSolidEntityByIdQuery({ id, qs }). - User edits and
updateSolidEntity({ id, data }). invalidatesTagsincludes both the{ id }and{ id: 'LIST' }tags so both detail and collection views are refreshed.
Troubleshooting
- Cache misses unexpectedly: Check your
qsserialization. Non-deterministic ordering or extra whitespace changes the key. - Queries not refetching after mutation: Verify
invalidatesTagsreturns the correct tag shape and yourtagTypesincludes the entity type. - Hook errors about conditional calls: Ensure hooks are called unconditionally; use
skipor lazy hooks instead. - Data seems stale: Consider
refetchOnFocus,refetchOnReconnect, or lowerkeepUnusedDataFor.
Summary (cheat sheet)
- Use
useGet...Query(arg)for automatic fetch; useuseLazy...Query()+trigger(arg)for manual fetch. - Build deterministic
qsstrings for caching.
Appendix: Quick copy-paste examples
List hook (eager):
const qs = qs.stringify({ offset: 0, limit: 10 }, { encode: false, arrayFormat: 'brackets' });
const { data } = useGetSolidEntitiesQuery(qs);
Get by id (eager):
const { data } = useGetSolidEntityByIdQuery({ id: 12, qs: qs.stringify({ fields: ['id','name'] }) });
Triggering lazy search:
const [trigger] = useLazyGetSolidEntitiesQuery();
<button onClick={() => trigger(qs)}>Search</button>