Claude Code for Astro DB: Type-Safe SQL in Astro with LibSQL — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Astro DB: Type-Safe SQL in Astro with LibSQL
Frontend

Claude Code for Astro DB: Type-Safe SQL in Astro with LibSQL

Published: February 5, 2027
Read time: 8 min read
By: Claude Skills 360

Astro DB adds a type-safe SQL database to Astro — powered by LibSQL in development and Astro Studio (or self-hosted Turso) in production. Tables are defined in db/config.ts with defineTable() and column helpers. db.select(), db.insert(), and db.update() provide a Drizzle-based query API with full TypeScript inference. Astro Actions handle type-safe form mutations — defineAction() with Zod input validation, called from <form> elements or JavaScript with actions.myAction(). Seed files populate the local database during development. Claude Code generates Astro DB table definitions, seed files, server-side queries in .astro pages, Astro Actions, and the Astro Studio configuration for production data-driven Astro applications.

CLAUDE.md for Astro DB Projects

## Astro DB Stack
- Version: astro >= 4.5, @astrojs/db >= 0.14
- Tables: define in db/config.ts with defineTable({ columns: { ... } })
- Queries: import { db, Table } from 'astro:db' — db.select().from(Table)
- Seed: db/seed.ts for local dev data — runs with astro dev
- Actions: define in src/actions/index.ts — export const server = { myAction: defineAction(...) }
- Drizzle: full drizzle-orm API works — eq(), and(), like(), orderBy()
- Production: astro db push (Astro Studio) or ASTRO_DB_REMOTE_URL (Turso/LibSQL)
- Migrations: astro db push --remote applies schema changes to remote

Table Definitions

// db/config.ts — Astro DB schema
import { defineDb, defineTable, column, sql } from "astro:db"

const Orders = defineTable({
  columns: {
    id: column.text({ primaryKey: true }),
    customerId: column.text({ references: () => Users.columns.id }),
    status: column.text({ default: "pending" }),
    totalCents: column.number(),
    items: column.json(),
    trackingNumber: column.text({ optional: true }),
    createdAt: column.date({ default: sql`CURRENT_TIMESTAMP` }),
    updatedAt: column.date({ default: sql`CURRENT_TIMESTAMP` }),
  },
  indexes: [
    { on: ["customerId"], unique: false },
    { on: ["status"], unique: false },
    { on: ["createdAt"], unique: false },
  ],
})

const Users = defineTable({
  columns: {
    id: column.text({ primaryKey: true }),
    email: column.text({ unique: true }),
    name: column.text(),
    role: column.text({ default: "customer" }),
    createdAt: column.date({ default: sql`CURRENT_TIMESTAMP` }),
  },
})

const Products = defineTable({
  columns: {
    id: column.text({ primaryKey: true }),
    name: column.text(),
    slug: column.text({ unique: true }),
    description: column.text({ optional: true }),
    priceCents: column.number(),
    stock: column.number({ default: 0 }),
    active: column.boolean({ default: true }),
  },
})

export default defineDb({
  tables: { Orders, Users, Products }
})

Seed File

// db/seed.ts — populate local dev database
import { db, Orders, Users, Products } from "astro:db"

export default async function () {
  // Seed users
  await db.insert(Users).values([
    { id: "user-1", email: "[email protected]", name: "Alice Smith", role: "customer" },
    { id: "user-2", email: "[email protected]", name: "Bob Jones", role: "customer" },
    { id: "admin-1", email: "[email protected]", name: "Admin User", role: "admin" },
  ])

  // Seed products
  await db.insert(Products).values([
    { id: "prod-1", name: "Widget Pro", slug: "widget-pro", priceCents: 2999, stock: 100 },
    { id: "prod-2", name: "Widget Basic", slug: "widget-basic", priceCents: 999, stock: 500 },
    { id: "prod-3", name: "Widget Bundle", slug: "widget-bundle", priceCents: 4999, stock: 50 },
  ])

  // Seed orders with JSON items
  await db.insert(Orders).values([
    {
      id: "order-1",
      customerId: "user-1",
      status: "delivered",
      totalCents: 3998,
      items: [
        { productId: "prod-1", name: "Widget Pro", quantity: 1, priceCents: 2999 },
        { productId: "prod-2", name: "Widget Basic", quantity: 1, priceCents: 999 },
      ],
    },
    {
      id: "order-2",
      customerId: "user-1",
      status: "pending",
      totalCents: 4999,
      items: [
        { productId: "prod-3", name: "Widget Bundle", quantity: 1, priceCents: 4999 },
      ],
    },
    {
      id: "order-3",
      customerId: "user-2",
      status: "shipped",
      totalCents: 5998,
      trackingNumber: "1Z999AA10123456784",
      items: [
        { productId: "prod-1", name: "Widget Pro", quantity: 2, priceCents: 2999 },
      ],
    },
  ])
}

Queries in Astro Pages

---
// src/pages/orders/index.astro — server-side Astro DB query
import { db, Orders, Users, and, eq, desc, like } from "astro:db"
import Layout from "../../layouts/Layout.astro"
import { actions } from "astro:actions"

const session = Astro.locals.session
if (!session?.userId) return Astro.redirect("/login")

const status = Astro.url.searchParams.get("status")
const search = Astro.url.searchParams.get("search")

// Build query with optional filters
let ordersQuery = db
  .select({
    orderId: Orders.id,
    status: Orders.status,
    totalCents: Orders.totalCents,
    items: Orders.items,
    createdAt: Orders.createdAt,
    customerName: Users.name,
    customerEmail: Users.email,
  })
  .from(Orders)
  .innerJoin(Users, eq(Orders.customerId, Users.id))
  .where(eq(Orders.customerId, session.userId))
  .orderBy(desc(Orders.createdAt))

if (status) {
  ordersQuery = ordersQuery.where(
    and(
      eq(Orders.customerId, session.userId),
      eq(Orders.status, status)
    )
  )
}

const orders = await ordersQuery.limit(50)
const totalSpent = orders.reduce((sum, o) => sum + o.totalCents, 0)
---

<Layout title="My Orders">
  <div class="orders-page">
    <h1>Orders</h1>
    <p>Total spent: ${(totalSpent / 100).toFixed(2)}</p>

    <div class="filters">
      <a href="?status=pending">Pending</a>
      <a href="?status=shipped">Shipped</a>
      <a href="/orders">All</a>
    </div>

    {orders.map(order => (
      <div class="order-card" key={order.orderId}>
        <span>#{order.orderId.slice(-8)}</span>
        <span>{order.status}</span>
        <span>${(order.totalCents / 100).toFixed(2)}</span>

        {order.status === "pending" && (
          <form method="POST" action={actions.cancelOrder}>
            <input type="hidden" name="orderId" value={order.orderId} />
            <button type="submit">Cancel</button>
          </form>
        )}
      </div>
    ))}
  </div>
</Layout>

Astro Actions

// src/actions/index.ts — type-safe server mutations
import { defineAction, ActionError } from "astro:actions"
import { z } from "astro:schema"  // or zod
import { db, Orders, eq } from "astro:db"

export const server = {
  cancelOrder: defineAction({
    accept: "form",  // Works with native <form> POST
    input: z.object({
      orderId: z.string().min(1),
    }),
    handler: async ({ orderId }, context) => {
      const session = context.locals.session
      if (!session?.userId) {
        throw new ActionError({ code: "UNAUTHORIZED" })
      }

      // Verify ownership
      const [order] = await db
        .select()
        .from(Orders)
        .where(eq(Orders.id, orderId))
        .limit(1)

      if (!order) {
        throw new ActionError({ code: "NOT_FOUND", message: "Order not found" })
      }

      if (order.customerId !== session.userId) {
        throw new ActionError({ code: "FORBIDDEN" })
      }

      if (order.status !== "pending") {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: `Cannot cancel order with status: ${order.status}`,
        })
      }

      await db
        .update(Orders)
        .set({ status: "cancelled", updatedAt: new Date() })
        .where(eq(Orders.id, orderId))

      return { cancelled: true, orderId }
    },
  }),

  createOrder: defineAction({
    accept: "json",
    input: z.object({
      items: z.array(z.object({
        productId: z.string(),
        quantity: z.number().int().positive(),
      })).min(1),
    }),
    handler: async ({ items }, context) => {
      const session = context.locals.session
      if (!session?.userId) throw new ActionError({ code: "UNAUTHORIZED" })

      // Compute total from DB prices
      const productIds = items.map(i => i.productId)

      const products = await db
        .select()
        .from(Products)
        .where(inArray(Products.id, productIds))

      const totalCents = items.reduce((sum, item) => {
        const product = products.find(p => p.id === item.productId)
        if (!product) throw new ActionError({ code: "BAD_REQUEST", message: `Product ${item.productId} not found` })
        return sum + product.priceCents * item.quantity
      }, 0)

      const orderId = crypto.randomUUID()

      await db.insert(Orders).values({
        id: orderId,
        customerId: session.userId,
        status: "pending",
        totalCents,
        items: items.map(item => ({
          ...item,
          name: products.find(p => p.id === item.productId)!.name,
          priceCents: products.find(p => p.id === item.productId)!.priceCents,
        })),
      })

      return { orderId }
    },
  }),
}

For the standalone Drizzle ORM + Turso setup that provides the same LibSQL queries outside of Astro’s framework constraints when building APIs rather than content sites, see the Turso guide for libSQL client configuration. For the SvelteKit equivalent with its own integrated form actions and database patterns using Drizzle, the SvelteKit guide covers load functions and form actions. The Claude Skills 360 bundle includes Astro DB skill sets covering table definitions, seed files, and Actions. Start with the free tier to try Astro DB schema generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free