Adding an API Service
How to add a new API domain with auto-generated query and mutation hooks.
1. Create the service file
# In packages/api/services/energy/
touch my-domain.service.ts2. Define types
// packages/api/services/energy/my-domain.service.ts
import {
createQueryGroup,
createMutationGroup,
queryEndpoint,
paginatedQueryEndpoint,
mutationEndpoint,
} from "../../helpers";
export interface TItem {
id: string;
name: string;
status: string;
createdAt: string;
}
export interface TItemCreateReq {
name: string;
status: string;
}
export interface TItemsListParams {
search?: string;
status?: string;
}3. Define queries
export const { useGetItemsQuery, useGetItemQuery } = createQueryGroup({
getItems: paginatedQueryEndpoint<TItem, TItemsListParams>({
url: "/energy/items",
paginated: { uniqueBy: "id" },
}),
getItem: queryEndpoint<TItem, { id: string }>({
url: ({ id }) => `/energy/items/${id}`,
}),
});4. Define mutations
export const {
useCreateItemMutation,
useUpdateItemMutation,
useDeleteItemMutation,
} = createMutationGroup({
createItem: mutationEndpoint<TItem, TItemCreateReq>({
url: "/energy/items",
method: "POST",
addCreatedToPaginatedList: {
listQueryKey: "getItems",
selectListItem: (data) => data,
},
}),
updateItem: mutationEndpoint<TItem, Partial<TItemCreateReq>, { id: string }>({
url: ({ id }) => `/energy/items/${id}`,
method: "PATCH",
mergeUpdatedInPaginatedList: {
listQueryKey: "getItems",
selectListItem: (data) => data,
},
}),
deleteItem: mutationEndpoint<void, void, { id: string }>({
url: ({ id }) => `/energy/items/${id}`,
method: "DELETE",
removeFromPaginatedList: {
listQueryKey: "getItems",
getDeletedId: (variables) =>
(variables as { params: { id: string } }).params.id,
},
}),
});5. Export from the domain index
// packages/api/services/energy/index.ts
export * from "./my-domain.service";6. Add sub-path export (if needed)
If the service is in a new directory, add the export path to packages/api/package.json:
{
"exports": {
"./services/energy": "./services/energy/index.ts"
}
}7. Use in components
"use client";
import { useGetItemsQuery, useCreateItemMutation } from "@numa/api/services/energy";
export default function ItemsList() {
const { data, isLoading } = useGetItemsQuery({ search: "" });
const { mutate: createItem, isPending } = useCreateItemMutation({
onSuccess: () => toast.success("Item created"),
});
return (
<>
{data?.items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={() => createItem({ body: { name: "New", status: "active" } })}>
Add
</button>
</>
);
}Hook naming convention
The key name becomes the hook name automatically:
| Key | Generated hook |
|---|---|
getItems | useGetItemsQuery |
getItem | useGetItemQuery |
createItem | useCreateItemMutation |
updateItem | useUpdateItemMutation |
deleteItem | useDeleteItemMutation |
Pattern: use + PascalCase(key) + Query or Mutation.
Cache invalidation
For cases where automatic list cache updates aren’t sufficient:
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
const { mutate } = useCreateItemMutation({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["getItems"] });
},
});Query keys use the endpoint key name, not the URL.
Last updated on