Formulář návštěvní knihy nad libsql, kde je každý záznam bezpečně ošetřen.
Návštěvníci napíšou jméno a zprávu a stránka vypíše všechny, kdo se podepsali. Nasazením tohoto příkladu vznikne libsql databáze, která se do služby propojí jako GUESTBOOK_DB, connection string bez tokenu; přihlašovací údaje vloží platforma až na hraně sítě, takže v kódu není žádné databázové heslo.
Služba si tabulku vytvoří na vyžádání pomocí CREATE TABLE IF NOT EXISTS, každý záznam ukládá parametrizovaným dotazem (vstup uživatele se nikdy nepovažuje za SQL) a před vykreslením ošetří každé jméno i zprávu (zpráva tak nemůže vložit HTML ani skript). Po úspěšném POSTu přesměruje, takže obnovení stránky formulář neodešle znovu.
// A classic guestbook: visitors leave a name and a message, and the page shows // everyone who signed. Data lives in a libsql database that was created and // linked for you at deploy time. Its connection string arrives in GUESTBOOK_DB // as a tokenless URL; the platform injects the credentials at the network edge, // so there is never a database secret in your code. import { createClient } from "npm:@libsql/client@0.14.0/web"; const db = createClient({ url: Deno.env.get("GUESTBOOK_DB")! }); type Entry = { name: string; message: string; created_at: string }; export default async (req: Request): Promise<Response> => { // Create the table on demand. IF NOT EXISTS makes this safe to run every time. await db.execute( `CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, message TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) )`, ); const { pathname } = new URL(req.url); if (req.method === "POST" && pathname === "/") { const form = await req.formData(); const name = String(form.get("name") ?? "").trim().slice(0, 80); const message = String(form.get("message") ?? "").trim().slice(0, 500); if (!name || !message) { // Expected validation miss: 422, not a 5xx, so the run is not marked failed. return render(await recentEntries(), "Please fill in both fields.", 422); } // Parameterised query: values are bound, never spliced into the SQL string. await db.execute({ sql: "INSERT INTO entries (name, message) VALUES (?, ?)", args: [name, message], }); // Redirect after POST so a page refresh does not submit the form again. return new Response(null, { status: 303, headers: { location: "/" } }); } if (req.method === "GET" && pathname === "/") { return render(await recentEntries()); } return new Response("Not found\n", { status: 404 }); }; async function recentEntries(): Promise<Entry[]> { const result = await db.execute( "SELECT name, message, created_at FROM entries ORDER BY id DESC LIMIT 100", ); return result.rows.map((row) => ({ name: String(row.name), message: String(row.message), created_at: String(row.created_at), })); } function render(entries: Entry[], notice = "", status = 200): Response { const list = entries.length === 0 ? `<p class="empty">No messages yet. Be the first to sign.</p>` : entries .map( (entry) => `<li> <p class="msg">${escapeHtml(entry.message)}</p> <p class="meta">${escapeHtml(entry.name)} · ${escapeHtml(entry.created_at)}</p> </li>`, ) .join("\n "); const body = `<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Guestbook</title> <style> body { max-width: 40rem; margin: 3rem auto; padding: 0 1.25rem; font: 16px/1.6 system-ui, sans-serif; color: #16181d; } h1 { letter-spacing: -0.02em; } form { display: grid; gap: 0.6rem; margin: 1.5rem 0 2rem; } input, textarea { font: inherit; padding: 0.55rem 0.7rem; border: 1px solid #d7dae0; border-radius: 6px; } button { justify-self: start; font: inherit; padding: 0.55rem 1rem; border: 0; border-radius: 6px; background: #16181d; color: #fff; cursor: pointer; } .notice { color: #b4232a; } ul { list-style: none; padding: 0; display: grid; gap: 1rem; } li { border: 1px solid #e6e8ec; border-radius: 8px; padding: 0.8rem 1rem; } .msg { margin: 0 0 0.35rem; } .meta { margin: 0; color: #5c636e; font-size: 0.85rem; } .empty { color: #5c636e; } </style> </head> <body> <h1>Guestbook</h1> ${notice ? `<p class="notice">${escapeHtml(notice)}</p>` : ""} <form method="post" action="/"> <input name="name" placeholder="Your name" maxlength="80" required /> <textarea name="message" placeholder="Say hello" rows="3" maxlength="500" required></textarea> <button type="submit">Sign the guestbook</button> </form> <ul> ${list} </ul> </body> </html> `; return new Response(body, { status, headers: { "content-type": "text/html; charset=utf-8" }, }); } // Never write user input into HTML without escaping it first, or a message like // "<script>...</script>" would run in every visitor's browser. function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }
Go beyond what seems possible.