Install
npx skillscat add yebot/rad-cc-plugins/plugins-astro-content-author-skills-astro-db-patterns Install via the SkillsCat registry.
SKILL.md
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
npx astro add dbConfiguration File
Create db/config.ts:
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
// 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
// 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
// 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
// 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
// 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
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
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
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:
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
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
import { db, Post } from 'astro:db';
const allPosts = await db.select().from(Post);Select with Filter
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 undefinedMultiple Conditions
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
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
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
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
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
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
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
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
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
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
// 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
// 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
// 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
// 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
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
ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
ASTRO_DB_APP_TOKEN=your-auth-tokenPush Schema to Production
astro db push --remoteVerify Remote Connection
astro db verify --remoteBest Practices
- Use Primary Keys: Always define a primary key for each table
- Unique Constraints: Use
unique: truefor fields like email, slug - Default Values: Provide sensible defaults for boolean/number fields
- Optional Fields: Mark truly optional fields with
optional: true - Indexing: Use unique constraints for fields you'll query often
- Relationships: Use foreign keys (number columns) to link tables
- Dates: Always use
column.date()for timestamp fields - JSON: Use JSON columns for flexible/nested data
- Naming: Use consistent naming (camelCase for columns, PascalCase for tables)
- Seeding: Keep seed data representative and minimal
Tips
- Dev server auto-restarts on schema changes
- Check
.astro/content.dbfor 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