- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.8 KiB
title, description, url
| title | description | url |
|---|---|---|
| Using API client | How to use API client to interact with the API. | /docs/extension/api/client |
Using API client
In browser extension code, you can only access the API client from the client-side.
When you create a new component or piece of your extension and want to fetch some data, you can use the API client to do so.
Creating a client
We're creating a client-side API client in apps/extension/src/lib/api/index.tsx file. It's a simple wrapper around the @tanstack/react-query that fetches or mutates data from the API.
It also requires wrapping your views in a QueryClientProvider component to provide the API client to the rest of the components.
We recommend to create a separate layout file, which will be used to wrap your pages. TurboStarter comes with a layout.tsx file in the modules/common/layout folder, which you can use as a template:
export const Layout = ({
children,
loadingFallback,
errorFallback,
}: LayoutProps) => {
return (
<ErrorBoundary fallback={errorFallback}>
<Suspense fallback={loadingFallback}>
<QueryClientProvider>{children}</QueryClientProvider>
</Suspense>
</ErrorBoundary>
);
};
Remember that every part of your extension will be mounted as a separate React component, so you need to wrap each of them in the QueryClientProvider component if you want to use the API client inside:
import { Layout } from "~/modules/common/layout/layout";
export default function Popup() {
return <Layout>{/* your popup code here */}</Layout>;
}
const getBaseUrl = () => {
return env.VITE_SITE_URL;
};
As you can see we're mostly relying on the environment variables to get it, so there shouldn't be any issues with it, but in case, please be aware where to find it 😉
Queries
Of course, everything comes already configured for you, so you just need to start using api in your components/screens.
For example, to fetch the list of posts you can use the useQuery hook:
import { api } from "~/lib/api";
export const Posts = () => {
const { data: posts, isLoading } = useQuery({
queryKey: ["posts"],
queryFn: async () => {
const response = await api.posts.$get();
if (!response.ok) {
throw new Error("Failed to fetch posts!");
}
return response.json();
},
});
if (isLoading) {
return <p>Loading...</p>;
}
/* do something with the data... */
return (
<div>
<p>{JSON.stringify(posts)}</p>
</div>
);
};
It's using the @tanstack/react-query useQuery API, so you shouldn't have any troubles with it.
Mutations
If you want to perform a mutation in your extension code, you can use the useMutation hook that comes straight from the integration with Tanstack Query:
import { api } from "~/lib/api";
export const CreatePost = () => {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: async (post: PostInput) => {
const response = await api.posts.$post(post);
if (!response.ok) {
throw new Error("Failed to create post!");
},
},
onSuccess: () => {
toast.success("Post created successfully!");
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return <form onSubmit={onSubmit(mutate)} />;
};
Here, we're also invalidating the query after the mutation is successful. This is a very important step to make sure that the data is updated in the UI.
Handling responses
As you can see in the examples above, the Hono RPC client returns a plain Response object, which you can use to get the data or handle errors. However, implementing this handling in every query or mutation can be tedious and will introduce unnecessary boilerplate in your codebase.
That's why we've developed the handle function that unwraps the response for you, handles errors, and returns the data in a consistent format. You can safely use it with any procedure from the API client:
<Tabs items={["Queries", "Mutations"]}> ```tsx // [!code word:handle] import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api";
export const Posts = () => {
const { data: posts, isLoading } = useQuery({
queryKey: ["posts"],
queryFn: handle(api.posts.$get),
});
if (isLoading) {
return <p>Loading...</p>;
}
/* do something with the data... */
return (
<div>
<p>{JSON.stringify(posts)}</p>
</div>
);
};
```
```tsx
// [!code word:handle]
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/client";
export const CreatePost = () => {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: handle(api.posts.$post),
onSuccess: () => {
toast.success("Post created successfully!");
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return <form onSubmit={onSubmit(mutate)} />;
};
```
With this approach, you can focus on the business logic instead of repeatedly writing code to handle API responses in your browser extension components, making your extension's codebase more readable and maintainable.
The same error handling and response unwrapping benefits apply whether you're building web, mobile, or extension interfaces - allowing you to keep your data fetching logic consistent across all platforms.