# Astro DB Patterns Skill Common patterns and examples for using Astro DB, including schema design, queries, and data seeding. ## When to Use This Skill - Setting up Astro DB in a project - Designing database schemas - Writing queries with Drizzle ORM - Seeding development data - Building API endpoints with database access ## Database Setup ### Install Astro DB ```bash npx astro add db ``` ### Configuration File Create `db/config.ts`: ```typescript import { defineDb, defineTable, column } from 'astro:db'; const MyTable = defineTable({ columns: { id: column.number({ primaryKey: true }), // ... other columns } }); export default defineDb({ tables: { MyTable } }); ``` ## Column Types ### Text Columns ```typescript // Basic text name: column.text() // Unique text email: column.text({ unique: true }) // Optional text bio: column.text({ optional: true }) // Text with default status: column.text({ default: 'active' }) ``` ### Number Columns ```typescript // Basic number age: column.number() // Primary key id: column.number({ primaryKey: true }) // Optional number rating: column.number({ optional: true }) // Number with default views: column.number({ default: 0 }) // Unique number userId: column.number({ unique: true }) ``` ### Boolean Columns ```typescript // Basic boolean published: column.boolean() // Boolean with default active: column.boolean({ default: true }) featured: column.boolean({ default: false }) // Optional boolean verified: column.boolean({ optional: true }) ``` ### Date Columns ```typescript // Basic date createdAt: column.date() // Optional date publishedAt: column.date({ optional: true }) // Date with default (requires runtime value) updatedAt: column.date({ default: new Date() }) ``` ### JSON Columns ```typescript // JSON data metadata: column.json() // JSON with default settings: column.json({ default: {} }) options: column.json({ default: [] }) // Optional JSON extra: column.json({ optional: true }) ``` ## Common Schema Patterns ### Blog Database ```typescript import { defineDb, defineTable, column } from 'astro:db'; const Author = defineTable({ columns: { id: column.number({ primaryKey: true }), name: column.text(), email: column.text({ unique: true }), bio: column.text({ optional: true }), avatar: column.text({ optional: true }), website: column.text({ optional: true }), createdAt: column.date() } }); const Post = defineTable({ columns: { id: column.number({ primaryKey: true }), title: column.text(), slug: column.text({ unique: true }), content: column.text(), excerpt: column.text({ optional: true }), published: column.boolean({ default: false }), publishedAt: column.date({ optional: true }), updatedAt: column.date({ optional: true }), authorId: column.number(), views: column.number({ default: 0 }), featured: column.boolean({ default: false }) } }); const Tag = defineTable({ columns: { id: column.number({ primaryKey: true }), name: column.text({ unique: true }), slug: column.text({ unique: true }) } }); const PostTag = defineTable({ columns: { postId: column.number(), tagId: column.number() } }); const Comment = defineTable({ columns: { id: column.number({ primaryKey: true }), postId: column.number(), author: column.text(), email: column.text(), content: column.text(), approved: column.boolean({ default: false }), createdAt: column.date() } }); export default defineDb({ tables: { Author, Post, Tag, PostTag, Comment } }); ``` ### E-commerce Database ```typescript const Product = defineTable({ columns: { id: column.number({ primaryKey: true }), name: column.text(), slug: column.text({ unique: true }), description: column.text(), price: column.number(), salePrice: column.number({ optional: true }), sku: column.text({ unique: true }), inStock: column.boolean({ default: true }), quantity: column.number({ default: 0 }), categoryId: column.number(), images: column.json({ default: [] }), createdAt: column.date() } }); const Category = defineTable({ columns: { id: column.number({ primaryKey: true }), name: column.text(), slug: column.text({ unique: true }), description: column.text({ optional: true }), parentId: column.number({ optional: true }) } }); const Order = defineTable({ columns: { id: column.number({ primaryKey: true }), orderNumber: column.text({ unique: true }), customerId: column.number(), total: column.number(), status: column.text({ default: 'pending' }), createdAt: column.date(), completedAt: column.date({ optional: true }) } }); const OrderItem = defineTable({ columns: { id: column.number({ primaryKey: true }), orderId: column.number(), productId: column.number(), quantity: column.number(), price: column.number() } }); export default defineDb({ tables: { Product, Category, Order, OrderItem } }); ``` ### User Management Database ```typescript const User = defineTable({ columns: { id: column.number({ primaryKey: true }), username: column.text({ unique: true }), email: column.text({ unique: true }), passwordHash: column.text(), firstName: column.text(), lastName: column.text(), role: column.text({ default: 'user' }), active: column.boolean({ default: true }), emailVerified: column.boolean({ default: false }), lastLogin: column.date({ optional: true }), createdAt: column.date() } }); const Session = defineTable({ columns: { id: column.text({ primaryKey: true }), userId: column.number(), expiresAt: column.date(), createdAt: column.date() } }); const UserProfile = defineTable({ columns: { userId: column.number({ unique: true }), bio: column.text({ optional: true }), avatar: column.text({ optional: true }), location: column.text({ optional: true }), website: column.text({ optional: true }), social: column.json({ default: {} }) } }); export default defineDb({ tables: { User, Session, UserProfile } }); ``` ## Data Seeding ### Basic Seed File Create `db/seed.ts`: ```typescript import { db, Author, Post, Tag } from 'astro:db'; export default async function seed() { // Insert authors await db.insert(Author).values([ { id: 1, name: 'John Doe', email: 'john@example.com', bio: 'Tech enthusiast and blogger', createdAt: new Date('2024-01-01') }, { id: 2, name: 'Jane Smith', email: 'jane@example.com', bio: 'Software developer', createdAt: new Date('2024-01-01') } ]); // Insert tags await db.insert(Tag).values([ { id: 1, name: 'Astro', slug: 'astro' }, { id: 2, name: 'JavaScript', slug: 'javascript' }, { id: 3, name: 'Tutorial', slug: 'tutorial' } ]); // Insert posts await db.insert(Post).values([ { id: 1, title: 'Getting Started with Astro', slug: 'getting-started-astro', content: 'Full post content here...', excerpt: 'Learn the basics of Astro', published: true, publishedAt: new Date('2024-01-15'), authorId: 1, views: 150, featured: true }, { id: 2, title: 'Advanced Astro Patterns', slug: 'advanced-astro', content: 'Advanced content...', published: false, authorId: 2, views: 0 } ]); } ``` ### Seed with Relationships ```typescript import { db, Post, Tag, PostTag, Comment } from 'astro:db'; export default async function seed() { // ... insert posts and tags ... // Link posts to tags (many-to-many) await db.insert(PostTag).values([ { postId: 1, tagId: 1 }, // Post 1 -> Astro { postId: 1, tagId: 3 }, // Post 1 -> Tutorial { postId: 2, tagId: 1 }, // Post 2 -> Astro { postId: 2, tagId: 2 } // Post 2 -> JavaScript ]); // Add comments await db.insert(Comment).values([ { id: 1, postId: 1, author: 'Reader', email: 'reader@example.com', content: 'Great post!', approved: true, createdAt: new Date() } ]); } ``` ## Query Patterns ### Select All ```typescript import { db, Post } from 'astro:db'; const allPosts = await db.select().from(Post); ``` ### Select with Filter ```typescript import { db, Post, eq } from 'astro:db'; // Published posts const publishedPosts = await db .select() .from(Post) .where(eq(Post.published, true)); // Post by slug const post = await db .select() .from(Post) .where(eq(Post.slug, 'my-post')) .get(); // Returns single result or undefined ``` ### Multiple Conditions ```typescript import { db, Post, eq, gt, and, or } from 'astro:db'; // AND condition const featuredPublished = await db .select() .from(Post) .where(and( eq(Post.published, true), eq(Post.featured, true) )); // OR condition const popularOrFeatured = await db .select() .from(Post) .where(or( gt(Post.views, 1000), eq(Post.featured, true) )); ``` ### Comparison Operators ```typescript import { db, Post, gt, gte, lt, lte, ne, like } from 'astro:db'; // Greater than const popularPosts = await db .select() .from(Post) .where(gt(Post.views, 100)); // Greater than or equal const recentPosts = await db .select() .from(Post) .where(gte(Post.publishedAt, new Date('2024-01-01'))); // Less than const drafts = await db .select() .from(Post) .where(lt(Post.views, 10)); // Not equal const notDrafts = await db .select() .from(Post) .where(ne(Post.published, false)); // LIKE pattern matching const astroPosts = await db .select() .from(Post) .where(like(Post.title, '%Astro%')); ``` ### Ordering Results ```typescript import { db, Post, desc, asc } from 'astro:db'; // Descending order const latestPosts = await db .select() .from(Post) .orderBy(desc(Post.publishedAt)); // Ascending order const oldestFirst = await db .select() .from(Post) .orderBy(asc(Post.createdAt)); // Multiple order columns const sorted = await db .select() .from(Post) .orderBy(desc(Post.featured), desc(Post.publishedAt)); ``` ### Limit and Offset ```typescript import { db, Post, desc } from 'astro:db'; // Latest 10 posts const latest = await db .select() .from(Post) .orderBy(desc(Post.publishedAt)) .limit(10); // Pagination const page = 2; const perPage = 10; const paginated = await db .select() .from(Post) .limit(perPage) .offset((page - 1) * perPage); ``` ### Joins ```typescript import { db, Post, Author, eq } from 'astro:db'; // Inner join const postsWithAuthors = await db .select() .from(Post) .innerJoin(Author, eq(Post.authorId, Author.id)); // Access joined data postsWithAuthors.forEach(row => { console.log(row.Post.title); console.log(row.Author.name); }); ``` ### Complex Join Query ```typescript import { db, Post, Author, Tag, PostTag, eq } from 'astro:db'; // Posts with authors and tags const postsWithDetails = await db .select({ post: Post, author: Author, tag: Tag }) .from(Post) .innerJoin(Author, eq(Post.authorId, Author.id)) .innerJoin(PostTag, eq(Post.id, PostTag.postId)) .innerJoin(Tag, eq(PostTag.tagId, Tag.id)); ``` ## Insert, Update, Delete ### Insert Single Record ```typescript import { db, Post } from 'astro:db'; await db.insert(Post).values({ title: 'New Post', slug: 'new-post', content: 'Content here', authorId: 1, published: false }); ``` ### Insert Multiple Records ```typescript await db.insert(Tag).values([ { name: 'Tag 1', slug: 'tag-1' }, { name: 'Tag 2', slug: 'tag-2' }, { name: 'Tag 3', slug: 'tag-3' } ]); ``` ### Update Records ```typescript import { db, Post, eq } from 'astro:db'; // Update single field await db .update(Post) .set({ views: 100 }) .where(eq(Post.id, 1)); // Update multiple fields await db .update(Post) .set({ published: true, publishedAt: new Date(), updatedAt: new Date() }) .where(eq(Post.slug, 'my-post')); ``` ### Delete Records ```typescript import { db, Post, Comment, eq, lt } from 'astro:db'; // Delete by ID await db .delete(Post) .where(eq(Post.id, 1)); // Delete with condition await db .delete(Comment) .where(lt(Comment.createdAt, new Date('2024-01-01'))); ``` ## API Endpoint Patterns ### GET All Items ```typescript // src/pages/api/posts.json.ts import type { APIRoute } from 'astro'; import { db, Post, desc } from 'astro:db'; export const GET: APIRoute = async () => { const posts = await db .select() .from(Post) .orderBy(desc(Post.publishedAt)); return new Response(JSON.stringify(posts), { status: 200, headers: { 'Content-Type': 'application/json' } }); }; ``` ### GET Single Item ```typescript // src/pages/api/posts/[slug].json.ts import type { APIRoute } from 'astro'; import { db, Post, eq } from 'astro:db'; export const GET: APIRoute = async ({ params }) => { const post = await db .select() .from(Post) .where(eq(Post.slug, params.slug)) .get(); if (!post) { return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify(post), { status: 200, headers: { 'Content-Type': 'application/json' } }); }; ``` ### POST Create Item ```typescript // src/pages/api/posts.json.ts import type { APIRoute } from 'astro'; import { db, Post } from 'astro:db'; export const POST: APIRoute = async ({ request }) => { try { const data = await request.json(); await db.insert(Post).values({ title: data.title, slug: data.slug, content: data.content, authorId: data.authorId, published: false, createdAt: new Date() }); return new Response(JSON.stringify({ success: true }), { status: 201, headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } }; ``` ### PUT/PATCH Update Item ```typescript // src/pages/api/posts/[id].json.ts import type { APIRoute } from 'astro'; import { db, Post, eq } from 'astro:db'; export const PUT: APIRoute = async ({ params, request }) => { const data = await request.json(); const id = parseInt(params.id); await db .update(Post) .set({ ...data, updatedAt: new Date() }) .where(eq(Post.id, id)); return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }; ``` ### DELETE Item ```typescript export const DELETE: APIRoute = async ({ params }) => { const id = parseInt(params.id); await db.delete(Post).where(eq(Post.id, id)); return new Response(null, { status: 204 }); }; ``` ## Production Deployment ### Environment Variables ```env # .env ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io ASTRO_DB_APP_TOKEN=your-auth-token ``` ### Push Schema to Production ```bash astro db push --remote ``` ### Verify Remote Connection ```bash astro db verify --remote ``` ## Best Practices 1. **Use Primary Keys**: Always define a primary key for each table 2. **Unique Constraints**: Use `unique: true` for fields like email, slug 3. **Default Values**: Provide sensible defaults for boolean/number fields 4. **Optional Fields**: Mark truly optional fields with `optional: true` 5. **Indexing**: Use unique constraints for fields you'll query often 6. **Relationships**: Use foreign keys (number columns) to link tables 7. **Dates**: Always use `column.date()` for timestamp fields 8. **JSON**: Use JSON columns for flexible/nested data 9. **Naming**: Use consistent naming (camelCase for columns, PascalCase for tables) 10. **Seeding**: Keep seed data representative and minimal ## Tips - Dev server auto-restarts on schema changes - Check `.astro/content.db` for local database - Use `.get()` for single results, omit for arrays - Drizzle ORM provides type-safe queries - Test queries in seed file first - Use transactions for related inserts - Consider indexes for frequently queried fields - Migrate carefully in production