feat(db): mesh data model — meshes, members, invites, audit log
- 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>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: Using API client
|
||||
description: How to use API client to interact with the API.
|
||||
url: /docs/web/api/client
|
||||
---
|
||||
|
||||
# Using API client
|
||||
|
||||
In Next.js, you can access the API client in two ways:
|
||||
|
||||
* **server-side**: in server components and API routes
|
||||
* **client-side**: in client components
|
||||
|
||||
When you create a new page and want to fetch data, you have flexibility in where to make the API calls. Server Components are great for initial data loading since the fetching happens during server-side rendering, eliminating an extra client-server round trip. The data is then efficiently streamed to the client.
|
||||
|
||||
By default in Next.js, every component is a Server Component. You can opt into client-side rendering by adding the `use client` directive at the top of a component file. Client Components are useful when you need interactive features or want to fetch data based on user interactions. While they're initially server-rendered, they're also hydrated and rendered on the client, allowing you to make API calls directly from the browser.
|
||||
|
||||
Let's explore both approaches to understand their differences and use cases.
|
||||
|
||||
## Server-side
|
||||
|
||||
We're creating a server-side API client inside `apps/web/src/lib/api/server.ts` file. The client automatically handles passing authentication headers from the user's session to secure API endpoints.
|
||||
|
||||
It's pre-configured with all the necessary setup, so you can start using it right away without any additional configuration.
|
||||
|
||||
Then, there is nothing simpler than calling the API from your server component:
|
||||
|
||||
```tsx title="page.tsx"
|
||||
import { api } from "~/lib/api/server";
|
||||
|
||||
export default async function MyServerComponent() {
|
||||
const response = await api.posts.$get();
|
||||
const posts = await response.json();
|
||||
|
||||
/* do something with the data... */
|
||||
return <div>{JSON.stringify(posts)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
<Card title="Next.js - Server components" description="nextjs.org" href="https://nextjs.org/docs/app/building-your-application/rendering/server-components" />
|
||||
|
||||
## Client-side
|
||||
|
||||
We're creating a separate client-side API client in `apps/web/src/lib/api/client.tsx` file. It's a simple wrapper around the [@tanstack/react-query](https://tanstack.com/query/latest/docs/framework/react/overview) that fetches or mutates data from the API.
|
||||
|
||||
It also requires wrapping your app in a `QueryClientProvider` component to provide the query client to the rest of the app:
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<QueryClientProvider>{children}</QueryClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Of course, it's all already configured for you, so you just need to start using `api` in your client components:
|
||||
|
||||
```tsx title="page.tsx"
|
||||
"use client";
|
||||
|
||||
import { api } from "~/lib/api/client";
|
||||
|
||||
export default function MyClientComponent() {
|
||||
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 <div>Loading...</div>;
|
||||
}
|
||||
|
||||
/* do something with the data... */
|
||||
return <div>{JSON.stringify(posts)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
<Card title="Next.js - Client components" description="nextjs.org" href="https://nextjs.org/docs/app/building-your-application/rendering/client-components" />
|
||||
|
||||
<Callout type="warn" title="Ensure correct API url">
|
||||
Inside the `apps/web/src/lib/api/utils.ts` we're calling a function to get base url of your api, so make sure it's set correctly (especially on production) and your API endpoint is corresponding with the name there.
|
||||
|
||||
```tsx title="utils.ts"
|
||||
export const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return window.location.origin;
|
||||
if (env.NEXT_PUBLIC_URL) return env.NEXT_PUBLIC_URL;
|
||||
if (env.VERCEL_URL) return `https://${env.VERCEL_URL}`;
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
};
|
||||
```
|
||||
|
||||
As you can see we're mostly relying on the [environment variables](/docs/web/configuration/environment-variables) to get it, so there shouldn't be any issues with it, but in case, please be aware where to find it 😉
|
||||
</Callout>
|
||||
|
||||
## Handling responses
|
||||
|
||||
As you can see in the examples above, the [Hono RPC](https://hono.dev/docs/guides/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={["Server-side", "Client-side"]}>
|
||||
<Tab value="Server-side">
|
||||
```tsx
|
||||
// [!code word:handle]
|
||||
import { handle } from "@turbostarter/api/utils";
|
||||
|
||||
import { api } from "~/lib/api/server";
|
||||
|
||||
export default async function MyServerComponent() {
|
||||
const posts = await handle(api.posts.$get)();
|
||||
|
||||
/* do something with the data... */
|
||||
return <div>{JSON.stringify(posts)}</div>;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Client-side">
|
||||
```tsx
|
||||
// [!code word:handle]
|
||||
|
||||
"use client";
|
||||
|
||||
import { handle } from "@turbostarter/api/utils";
|
||||
|
||||
import { api } from "~/lib/api/client";
|
||||
|
||||
export default function MyClientComponent() {
|
||||
const { data: posts, isLoading } = useQuery({
|
||||
queryKey: ["posts"],
|
||||
queryFn: handle(api.posts.$get),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
/* do something with the data... */
|
||||
return <div>{JSON.stringify(posts)}</div>;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Internationalization
|
||||
description: Learn how to localize and translate your API.
|
||||
url: /docs/web/api/internationalization
|
||||
---
|
||||
|
||||
# Internationalization
|
||||
|
||||
Since TurboStarter provides fully featured [internationalization](/docs/web/internationalization/overview) out of the box, you can easily localize not only the frontend but also the API layer. This can be useful when you need to fetch localized data from the database or send emails in different languages.
|
||||
|
||||
Let's explore possibilities of this feature.
|
||||
|
||||
## Request-based localization
|
||||
|
||||
To get the locale for the current request, you can leverage the `localize` middleware:
|
||||
|
||||
```ts title="email/router.ts"
|
||||
const emailRouter = new Hono().get("/", localize, (c) => {
|
||||
const locale = c.var.locale;
|
||||
|
||||
// do something with the locale
|
||||
});
|
||||
```
|
||||
|
||||
Inside it, we're setting the `locale` variable in the current request context, making it available to the procedure.
|
||||
|
||||
## Error handling
|
||||
|
||||
When handling errors in an internationalized API, you'll want to ensure error messages are properly translated for your users. TurboStarter provides built-in support for localizing error messages using error codes and a special `onError` hook.
|
||||
|
||||
That's why it's recommended to use error codes instead of direct messages in your throw statements:
|
||||
|
||||
```ts
|
||||
throw new HttpException(HttpStatusCode.UNAUTHORIZED, {
|
||||
code: "auth:error.unauthorized",
|
||||
/* 👇 optional */
|
||||
message: "You are not authorized to access this resource.",
|
||||
});
|
||||
```
|
||||
|
||||
The error code will then be used to retrieve the localized message, and the returned response from your API will look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "auth:error.unauthorized",
|
||||
/* 👇 localized based on request's locale */
|
||||
"message": "You are not authorized to access this resource.",
|
||||
"path": "/api/auth/login",
|
||||
"status": 401,
|
||||
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
Then, you can either use the returned code to get the localized message in your frontend, or simply use the returned message as is.
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: Mutations
|
||||
description: Learn how to mutate data on the server.
|
||||
url: /docs/web/api/mutations
|
||||
---
|
||||
|
||||
# Mutations
|
||||
|
||||
As we saw in [adding new endpoint](/docs/web/api/new-endpoint#maybe-mutation), mutations allow us to modify data on the server, like creating, updating, or deleting resources. They can be defined similarly to queries using our API client.
|
||||
|
||||
Just like queries, mutations can be executed either server-side or client-side depending on your needs. Let's explore both approaches.
|
||||
|
||||
## Server actions
|
||||
|
||||
Next.js provides [server actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) as a powerful way to handle mutations directly on the server. They're particularly well-suited for form submissions and other data modifications.
|
||||
|
||||
Using our `api` client with server actions is straightforward - you simply call the API function on the server.
|
||||
|
||||
Here's an example of how you can define an action to create a new post:
|
||||
|
||||
<Tabs items={["With helper", "Without helper"]}>
|
||||
<Tab value="With helper">
|
||||
```tsx
|
||||
// [!code word:handle]
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { handle } from "@turbostarter/api/utils";
|
||||
|
||||
import { api } from "~/lib/api/server";
|
||||
|
||||
export async function createPost(post: PostInput) {
|
||||
try {
|
||||
await handle(api.posts.$post)(post);
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
|
||||
revalidatePath("/posts");
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Without helper">
|
||||
```tsx
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { api } from "~/lib/api/server";
|
||||
|
||||
export async function createPost(post: PostInput) {
|
||||
const response = await api.posts.$post(post);
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: "Failed to create post" };
|
||||
}
|
||||
|
||||
revalidatePath("/posts");
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
In the above example we're also using `revalidatePath` to revalidate the path `/posts` to fetch the updated list of posts.
|
||||
|
||||
<Cards>
|
||||
<Card title="Server actions and mutation" description="nextjs.org" href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations" />
|
||||
|
||||
<Card title="revalidatePath" description="nextjs.org" href="https://nextjs.org/docs/app/api-reference/functions/revalidatePath" />
|
||||
</Cards>
|
||||
|
||||
## useMutation hook
|
||||
|
||||
On the other hand, if you want to perform a mutation on the client-side, you can use the `useMutation` hook that comes straight from the integration with [React Query](https://tanstack.com/query).
|
||||
|
||||
<Tabs items={["With helper", "Without helper"]}>
|
||||
<Tab value="With helper">
|
||||
```tsx
|
||||
// [!code word:handle]
|
||||
import { handle } from "@turbostarter/api/utils";
|
||||
|
||||
import { api } from "~/lib/api/react";
|
||||
|
||||
export function 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={...} />;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Without helper">
|
||||
```tsx
|
||||
import { api } from "~/lib/api/react";
|
||||
|
||||
export function 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={...} />;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Cards>
|
||||
<Card title="useMutation hook" description="tanstack.com" href="https://tanstack.com/query/latest/docs/framework/react/reference/useMutation" />
|
||||
|
||||
<Card title="Query invalidation" description="tanstack.com" href="https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation" />
|
||||
</Cards>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Adding new endpoint
|
||||
description: How to add new endpoint to the API.
|
||||
url: /docs/web/api/new-endpoint
|
||||
---
|
||||
|
||||
# Adding new endpoint
|
||||
|
||||
To define a new API endpoint, you can either extend an existing entity (e.g. add new customer route) or create a new, separate module.
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
## Create new module
|
||||
|
||||
To create a new module you can create a new folder in the `modules` folder. For example `modules/posts`.
|
||||
|
||||
Then you would need to create a router declaration for this module. We're following a convention with the filename describing its purpose, so you would need to create a file named `router.ts` in the `modules/posts` folder.
|
||||
|
||||
```typescript title="modules/posts/router.ts"
|
||||
import { Hono } from "hono";
|
||||
|
||||
import { validate } from "../../middleware";
|
||||
|
||||
export const postsRouter = new Hono().get(
|
||||
"/",
|
||||
validate("query", filtersSchema),
|
||||
(c) => getAllPosts(c.req.valid("query")),
|
||||
);
|
||||
```
|
||||
|
||||
As you can see we're implementing a `.get` method without any additional middlewares for the router. This is a simple way to define a new GET endpoint.
|
||||
|
||||
Also, we're using a [zod](https://zod.dev/) validator to ensure that input passed to the endpoint is correct.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Maybe mutation?
|
||||
|
||||
The same way you can define a mutation for the new entity, just by changing the `get` to `post`:
|
||||
|
||||
```ts title="modules/posts/router.ts"
|
||||
// [!code word:.post]
|
||||
export const postsRouter = new Hono().post(
|
||||
"/",
|
||||
enforceAuth,
|
||||
validate("json", postSchema),
|
||||
(c) => createPost(c.req.valid("json")),
|
||||
);
|
||||
```
|
||||
|
||||
Hono supports all [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods), so you can define a new endpoint for any method you need (e.g. `put`, `delete`, etc.).
|
||||
|
||||
The `enforceAuth` middleware ensures that only authenticated users can access the endpoint, while the zod validator checks if the input data matches the expected schema. This combination provides both authentication and data validation in a single, clean setup.
|
||||
|
||||
[Read more about protected routes](/docs/web/api/protected-routes).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Implement logic
|
||||
|
||||
Then you would need to create a controller for this module. There is a place, where the logic happens, e.g. for the `GET /` endpoint we would need to create a `getAllPosts` function which will fetch posts from the database.
|
||||
|
||||
```typescript title="modules/posts/queries.ts"
|
||||
import { db } from "@turbostarter/db/server";
|
||||
import { posts } from "@turbostarter/db/schema";
|
||||
|
||||
export const getAllPosts = (filters: Filters) => {
|
||||
return db.select().from(posts).all().where(/* your filter logic here */);
|
||||
};
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Register router
|
||||
|
||||
To make the module and its endpoints available in the API you need to register a router for this module in the `index.ts` file:
|
||||
|
||||
```ts title="index.ts"
|
||||
import { postsRouter } from "./modules/posts/router";
|
||||
|
||||
const appRouter = new Hono()
|
||||
.basePath("/api")
|
||||
.route("/posts", postsRouter)
|
||||
/* other routers from your app logic */
|
||||
.onError(onError);
|
||||
|
||||
type AppRouter = typeof appRouter;
|
||||
|
||||
export type { AppRouter };
|
||||
export { appRouter };
|
||||
```
|
||||
|
||||
The `basePath` method sets a prefix for all routes in this router. While optional, using it helps organize API endpoints. This modular approach makes the API structure clearer and easier to maintain.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
That's it! You've just created a new API endpoint - it's now available at `/api/posts` 🎉
|
||||
|
||||
<Callout title="It's fully type-safe!">
|
||||
By exporting the `AppRouter` type you get fully type-safe RPC calls in the
|
||||
client. It's important because without producing a huge amount of code, we're
|
||||
fully type-safe from the frontend code. It helps avoid passing incorrect data
|
||||
to the procedure and streamline consuming returned types without a need to
|
||||
define these types by hand.
|
||||
</Callout>
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Overview
|
||||
description: Get started with the API.
|
||||
url: /docs/web/api/overview
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
TurboStarter is designed to be a scalable and production-ready full-stack starter kit. One of its core features is a dedicated and extensible API layer. To enable this in a type-safe manner, we chose [Hono](https://hono.dev) as the API server and client library.
|
||||
|
||||
<Callout title="Why Hono?">
|
||||
Hono is a small, simple, and ultrafast web framework that gives you a way to
|
||||
define your API endpoints with full type safety. It provides built-in
|
||||
middleware for common needs like validation, caching, and CORS.
|
||||
|
||||
It also
|
||||
includes an [RPC client](https://hono.dev/docs/guides/rpc) for making
|
||||
type-safe function calls from the frontend. Being edge-first, it's optimized
|
||||
for serverless environments and offers excellent performance.
|
||||
</Callout>
|
||||
|
||||
All API endpoints and their resolvers are defined in the `packages/api` package. Here you will find a `modules` folder that contains the different feature modules of the API. Each module has its own folder and exports all its resolvers.
|
||||
|
||||
For each module, we create a separate Hono router and then aggregate all sub-routers into one main router in the `index.ts` file.
|
||||
|
||||
The API is then exposed as a route handler that will be provided as a Next.js API route:
|
||||
|
||||
```ts title="apps/web/src/app/api/[...route]/route.ts"
|
||||
import { handle } from "hono/vercel";
|
||||
|
||||
import { appRouter } from "@turbostarter/api";
|
||||
|
||||
const handler = handle(appRouter);
|
||||
export {
|
||||
handler as GET,
|
||||
handler as POST,
|
||||
handler as OPTIONS,
|
||||
handler as PUT,
|
||||
handler as PATCH,
|
||||
handler as DELETE,
|
||||
handler as HEAD,
|
||||
};
|
||||
```
|
||||
|
||||
<Callout type="warn" title="API availability">
|
||||
The API is a part of web, serverless Next.js app. It means that you **must**
|
||||
deploy it to use the API in other apps (e.g. mobile app, browser extension),
|
||||
even if you don't need web app itself. It's very simple, as you're just
|
||||
deploying the Next.js app and the API is just a part of it.
|
||||
</Callout>
|
||||
|
||||
Learn more about API in the following sections:
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: Protected routes
|
||||
description: Learn how to protect your API routes.
|
||||
url: /docs/web/api/protected-routes
|
||||
---
|
||||
|
||||
# Protected routes
|
||||
|
||||
Hono has built-in support for [middlewares](https://hono.dev/docs/guides/middleware), which are functions that can be used to modify the context or execute code before or after a route handler is executed.
|
||||
|
||||
That's how we can secure our API endpoints from unauthorized access. Below are some examples of you can leverage middlewares to protect your API routes.
|
||||
|
||||
## Authenticated access
|
||||
|
||||
After validating the user's authentication status, we store their data in the context using [Hono's built-in context](https://hono.dev/docs/api/context). This allows us to access the user's information in subsequent middleware and procedures without having to re-validate the session.
|
||||
|
||||
Here's an example of middleware that validates whether the user is currently logged in and stores their data in the context:
|
||||
|
||||
```ts title="middleware.ts"
|
||||
export const enforceAuth = createMiddleware<{
|
||||
Variables: {
|
||||
user: User;
|
||||
};
|
||||
}>(async (c, next) => {
|
||||
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
||||
const user = session?.user ?? null;
|
||||
|
||||
if (!user) {
|
||||
throw new HTTPException(HttpStatusCode.UNAUTHORIZED, {
|
||||
message: "You need to be logged in to access this feature!",
|
||||
});
|
||||
}
|
||||
|
||||
c.set("user", user);
|
||||
await next();
|
||||
});
|
||||
```
|
||||
|
||||
Then we can use our defined middleware to protect endpoints by adding it before the route handler:
|
||||
|
||||
```ts title="billing/router.ts"
|
||||
export const billingRouter = new Hono().get(
|
||||
"/customer",
|
||||
enforceAuth,
|
||||
async (c) => c.json(await getCustomerByUserId(c.var.user.id)),
|
||||
);
|
||||
```
|
||||
|
||||
## Role-based access
|
||||
|
||||
In most cases, you will want to restrict access to certain endpoints based on the user's role.
|
||||
|
||||
You can achieve this by creating a middleware that will check if the user has the required role and then pass the execution to the next middleware or procedure.
|
||||
|
||||
E.g. for admin endpoints we want to ensure that the user has the `admin` role:
|
||||
|
||||
```ts title="middleware.ts"
|
||||
export const enforceAdmin = createMiddleware<{
|
||||
Variables: {
|
||||
user: User;
|
||||
};
|
||||
}>(async (c, next) => {
|
||||
const user = c.var.user;
|
||||
|
||||
if (!hasAdminPermission(user)) {
|
||||
throw new HttpException(HttpStatusCode.FORBIDDEN, {
|
||||
message: "You need to be an admin to access this feature!",
|
||||
});
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
```
|
||||
|
||||
Then we can use our defined middleware to protect endpoints by adding it before the route handler:
|
||||
|
||||
```ts title="admin/router.ts"
|
||||
export const adminRouter = new Hono().get(
|
||||
"/users",
|
||||
enforceAuth,
|
||||
enforceAdmin,
|
||||
(c) => c.json(...),
|
||||
);
|
||||
```
|
||||
|
||||
## Feature-based access
|
||||
|
||||
When developing your API you may want to restrict access to certain features based on the user's current subscription plan. (e.g. only users with "Pro" plan can access teams).
|
||||
|
||||
You can achieve this by creating a middleware that will check if the user has access to the feature and then pass the execution to the next middleware or procedure:
|
||||
|
||||
```ts title="middleware.ts"
|
||||
export const enforceFeatureAvailable = (feature: Feature) =>
|
||||
createMiddleware<{
|
||||
Variables: {
|
||||
user: User;
|
||||
};
|
||||
}>(async (c, next) => {
|
||||
const { data: customer } = await getCustomerById(c.var.user.id);
|
||||
|
||||
const hasFeature = isFeatureAvailable(customer, feature);
|
||||
|
||||
if (!hasFeature) {
|
||||
throw new HTTPException(HttpStatusCode.PAYMENT_REQUIRED, {
|
||||
message: "Upgrade your plan to access this feature!",
|
||||
});
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
```
|
||||
|
||||
Use it within your procedure the same way as we did with `enforceAuth` middleware:
|
||||
|
||||
```ts title="teams/router.ts"
|
||||
export const teamsRouter = new Hono().get(
|
||||
"/",
|
||||
enforceAuth,
|
||||
enforceFeatureAvailable(FEATURES.PRO.TEAMS),
|
||||
(c) => c.json(...),
|
||||
);
|
||||
```
|
||||
|
||||
These are just examples of what you can achieve with Hono middlewares. You can use them to add any kind of logic to your API (e.g. [logging](https://hono.dev/docs/middleware/builtin/logger), [caching](https://hono.dev/docs/middleware/builtin/cache), etc.)
|
||||
Reference in New Issue
Block a user