Vue
Build a Vue 3 contact list on AgentDB. Vue runs in the browser, so it calls your backend. The backend holds the AgentDB key.
Not run in CI. The Vue component is not run in the test suite. The backend it calls is the html-node-backend starter, which the smoke test runs.
#Where the key lives
On the backend, in its environment. Vite puts every VITE_ variable into the browser bundle, so the key never gets that prefix. Nuxt does the same with NUXT_PUBLIC_ and runtimeConfig.public.
VITE_AGENTDB_TOKEN=adbk.your-key# Read by the backend (or by Nuxt server routes through private runtimeConfig).
AGENTDB_TOKEN=replace-with-your-agent-key#The backend route
Use the Node server of the plain HTML guide, or any backend in this section. It serves GET /api/contacts?q= and POST /api/contacts.
const ALLOWED = {
"GET /api/contacts": async (url) => {
const q = (url.searchParams.get("q") ?? "").trim().slice(0, 100);
const data = await agentdb("POST", "/query", {
object: "contact",
where: { all: q ? [{ field: "name", op: "contains", value: q }] : [] },
sort: [{ field: "name", order: "asc" }],
limit: 50,
});
return [200, { contacts: data.records.map((r) => ({ id: r.id, name: r.data.name, email: r.data.email ?? null })) }];
},
"POST /api/contacts": async (_url, body, req) => {
const name = String(body.name ?? "").trim().slice(0, 200);
const email = String(body.email ?? "").trim().slice(0, 254);
if (!name) return [400, { error: "A contact needs a name." }];
// One id per submit from the page: a retried request is not written twice.
const submit = String(req.headers["x-submit-id"] ?? randomUUID()).replace(/[^A-Za-z0-9-]/g, "").slice(0, 64);
const rec = await agentdb("POST", "/records", { object: "contact", data: { name, ...(email && { email }) } }, `html-node.${submit}`);
return [201, { contact: { id: rec.id, name: rec.data.name, email: rec.data.email ?? null } }];
},
};With Vite, forward /api to it during development: server: { proxy: { "/api": "http://localhost:3000" } } in vite.config.js.
With Nuxt, put the AgentDB call in server/api/contacts.get.ts and read the key from private useRuntimeConfig() values. Do not use runtimeConfig.public for it.
#The frontend
<script setup>
import { ref, watch } from "vue";
const q = ref("");
const contacts = ref([]);
const status = ref("loading"); // loading | ready | error
const error = ref("");
let controller;
async function load() {
controller?.abort();
controller = new AbortController();
status.value = "loading";
try {
const res = await fetch(`/api/contacts?q=${encodeURIComponent(q.value)}`, { signal: controller.signal });
const body = await res.json();
if (!res.ok) throw new Error(body.error ?? "Could not load contacts.");
contacts.value = body.contacts;
status.value = "ready";
} catch (e) {
if (e.name === "AbortError") return;
error.value = e.message;
status.value = "error";
}
}
watch(q, load, { immediate: true });
</script>
<template>
<input v-model.trim="q" type="search" aria-label="Search by name" />
<p v-if="status === 'loading'" role="status">Loading…</p>
<p v-else-if="status === 'error'" role="alert">Error: {{ error }}</p>
<p v-else-if="contacts.length === 0">No contacts found.</p>
<ul>
<li v-for="c in contacts" :key="c.id">{{ c.name }}</li>
</ul>
</template>{{ c.name }} is shown as text. Do not use v-html with data from AgentDB.
#Loading, empty and error states
One status ref drives the page. "No contacts" shows only when the status is ready and the list is empty.
#Live updates
Add an /events route to your backend (see the live dashboard starter). In the component, open new EventSource("/events") in onMounted and close it in onUnmounted. See Live data.