Nuxt
Run bknd inside Nuxt
Installation
To get started with Nuxt and bknd, create a new Nuxt project by following the official guide, and then install bknd as a dependency:
npm install bkndpnpm install bkndyarn add bkndbun add bkndConfiguration
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running node -v, and
upgrade if necessary.
Now create a bknd.config.ts file in the root of your project:
import type NuxtBkndConfig from "bknd/adapter/nuxt";
import { em, entity, text, boolean } from "bknd";
import { secureRandomString } from "bknd/utils";
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
export default {
connection: {
url: "file:data.db",
},
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
secret: secureRandomString(32),
},
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
},
} satisfies NuxtBkndConfig;For more information about the connection object, refer to the Database guide.
See bknd.config.ts for more information on how to configure bknd. The NuxtBkndConfig type extends the base config type with the following properties:
export type NuxtBkndConfig<Env = NuxtEnv> = FrameworkBkndConfig<Env>;Serve the API and Admin UI
The Nuxt adapter uses Nuxt middleware to handle API requests and serve the Admin UI. Create a /server/middleware/bknd.ts file:
import { serve } from "bknd/adapter/nuxt";
import config from "../../bknd.config";
const handler = serve(config, process.env);
export default defineEventHandler(async (event) => {
const pathname = event.path;
const request = toWebRequest(event);
if (pathname.startsWith("/api") || pathname !== "/") {
const res = await handle(request);
if (res && res.status !== 404) {
return res;
}
}
});You can visit https://localhost:3000/admin to see the admin UI. Additionally you can create more todos as you explore the admin UI.
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configuration from the bknd.config.ts file:
import { type NuxtBkndConfig, getApp as getNuxtApp } from "bknd/adapter/nuxt";
import bkndConfig from "../../bknd.config";
export async function getApp<Env = NodeJS.ProcessEnv>(
config: NuxtBkndConfig<Env>,
args: Env = process.env as Env,
) {
return await getNuxtApp(config, args);
}
export async function getApi({ headers, verify }: { verify?: boolean; headers?: Headers }) {
const app = await getApp(bkndConfig, process.env);
if (verify) {
const api = app.getApi({ headers });
await api.verifyAuth();
return api;
}
return app.getApi();
};The adapter uses process.env to access environment variables, this works because Nuxt uses Nitro underneath and it will use polyfills for process.env making it platform/runtime agnostic.
Example usage of the API
You can use the getApp function to access the bknd API in your app to expose endpoints,
Here are some examples:
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { data, action } = body;
const api = await getApi({});
switch (action) {
case 'get': {
const limit = 5;
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
return { total: todos.body.meta.total, todos, limit };
}
case 'create': {
return await api.data.createOne("todos", { title: data.title });
}
case 'delete': {
return await api.data.deleteOne("todos", data.id);
}
case 'toggle': {
return await api.data.updateOne("todos", data.id, { done: !data.done });
}
default: {
return { path: action };
}
}
});This can't be done in the server/api directory as it will collide with the API endpoints created by the middleware. We will use defineEventHandler in the server/routes directory to create endpoints and use them in conjunction with composables to access the API safely.
Using the API through composables
To use the API in your frontend components/pages, you can create a composable that uses the enpoints created in previous steps:
import type { DB } from "bknd";
type Todo = DB["todos"];
export const useTodoActions = () => {
const fetchTodos = () =>
$fetch<{ limit: number; todos: Array<Todo>; total: number }>("/todos", {
method: "POST",
body: { action: "get" },
});
const createTodo = (title: string) =>
$fetch("/todos", {
method: "POST",
body: { action: "create", data: { title } },
});
const deleteTodo = (todo: Todo) =>
$fetch("/todos", {
method: "POST",
body: { action: "delete", data: { id: todo.id } },
});
const toggleTodo = (todo: Todo) =>
$fetch("/todos", {
method: "POST",
body: { action: "toggle", data: todo },
});
return { fetchTodos, createTodo, deleteTodo, toggleTodo };
};Usage in a page/component
Make a composable to fetch the todos:
import type { DB } from "bknd";
type Todo = DB["todos"];
export const useTodoActions = () => {
const fetchTodos = () =>
$fetch<{ limit: number; todos: Array<Todo>; total: number }>("/todos", {
method: "POST",
body: { action: "get" },
});
const createTodo = (title: string) =>
$fetch("/todos", {
method: "POST",
body: { action: "create", data: { title } },
});
const deleteTodo = (todo: Todo) =>
$fetch("/todos", {
method: "POST",
body: { action: "delete", data: { id: todo.id } },
});
const toggleTodo = (todo: Todo) =>
$fetch("/todos", {
method: "POST",
body: { action: "toggle", data: todo },
});
return { fetchTodos, createTodo, deleteTodo, toggleTodo };
};Then use the useTodoActions composable in a page:
<script lang="ts" setup>
const { fetchTodos, createTodo, deleteTodo, toggleTodo } = useTodoActions();
const { data:todos, execute } = await useAsyncData("todos", () => fetchTodos());
onMounted(() => {
execute();
});
</script>
<template>
<div
v-if="todos"
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
>
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
<div class="flex flex-row items-center ">
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
<div class="ml-3.5 mr-2 font-mono opacity-70">&</div>
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
</div>
<div v-if="data?.todos">
<ul>
<li v-for="todo in data.todos" :key="todo.id">
{{ todo.title }}
<button @click="toggleTodo(todo)">Toggle</button>
<button @click="deleteTodo(todo)">Delete</button>
</li>
</ul>
<form @submit.prevent="createTodo('New Todo')">
<input type="text" placeholder="New Todo" />
<button type="submit">Add</button>
</form>
</div>
<div v-else className="flex flex-col gap-1">
<p>
No todos found.
</p>
</div>
</main>
</div>
</template>You can visit https://localhost:3000/todos to see all the todos.
Using authentication
Make a composable to fetch the user:
import type { User } from "bknd";
export const useUser = () => {
const getUser = () => $fetch("/api/auth/me") as Promise<{ user: User }>;
return { getUser };
};Then use the useUser composable in a page:
<script lang="ts" setup>
const { getUser } = useUser();
const { data, status: userStatus, execute } = await useAsyncData("user", () => getUser());
onMounted(() => {
execute();
});
</script>
<template>
<div
v-if="userStatus !== 'pending'"
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
>
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
<div class="flex flex-row items-center ">
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
<div class="ml-3.5 mr-2 font-mono opacity-70">&</div>
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
</div>
<div v-if="data?.user">
Logged in as {{ data.user.email }}.
<a className="font-medium underline" href='/api/auth/logout'>
Logout
</a>
</div>
<div v-else className="flex flex-col gap-1">
<p>
Not logged in.
<a className="font-medium underline" href="/admin/auth/login">
Login
</a>
</p>
<p className="text-xs opacity-50">
Sign in with:
<b>
<code>test@bknd.io</code>
</b>
/
<b>
<code>12345678</code>
</b>
</p>
</div>
</main>
<Footer />
</div>
</template>Important Note
Use external attribute on Nuxt links, anytime you are traversing to an external route (like /api/* which are handled by bknd's middleware) to prevent vue router from intercepting the link.
<NuxtLink external href="/admin">
Admin
</NuxtLink>If you don't use the external attribute, vue router will intercept the link and try to navigate to it, which will fail and result in a 404 error.
Check the Nuxt repository example for more implementation details.