Examples and Use Cases
Explore practical examples and common use cases for building applications with SonicJS. Each example includes code snippets, configuration, and best practices.
Overview
SonicJS is versatile enough to power various types of applications, from simple blogs to complex multi-tenant platforms. This guide showcases common patterns and implementations.
What You'll Learn
- Setting up a blog with SEO optimization
- Building an e-commerce product catalog
- Creating a documentation site with search
- Implementing multi-tenant architecture
- API-first headless CMS patterns
- Custom content workflows
Blog Setup
Create a fully-featured blog with categories, tags, authors, and SEO optimization.
Collection Schema
Blog Collections
// Blog post collection
{
id: 'blog_posts',
name: 'Blog Posts',
fields: [
{
name: 'title',
type: 'text',
required: true,
validation: { minLength: 5, maxLength: 200 }
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
pattern: '^[a-z0-9-]+$'
},
{
name: 'content',
type: 'richtext',
required: true
},
{
name: 'excerpt',
type: 'textarea',
maxLength: 300
},
{
name: 'featuredImage',
type: 'media',
mediaTypes: ['image']
},
{
name: 'author',
type: 'relation',
collection: 'authors',
required: true
},
{
name: 'categories',
type: 'relation',
collection: 'categories',
multiple: true
},
{
name: 'tags',
type: 'tags'
},
{
name: 'publishedAt',
type: 'datetime'
},
{
name: 'status',
type: 'select',
options: ['draft', 'published', 'archived'],
default: 'draft'
},
{
name: 'seo',
type: 'group',
fields: [
{ name: 'metaTitle', type: 'text', maxLength: 60 },
{ name: 'metaDescription', type: 'textarea', maxLength: 160 },
{ name: 'ogImage', type: 'media' }
]
}
]
}
API Implementation
Blog API Routes
import { Hono } from 'hono'
import { DocumentRepository } from '@sonicjs-cms/core'
const blog = new Hono()
// Get all published posts with pagination
// Uses the SonicJS public API — GET /api/blog_posts?status=published
blog.get('/posts', async (c) => {
const page = parseInt(c.req.query('page') || '1')
const limit = parseInt(c.req.query('limit') || '10')
const category = c.req.query('category')
const tag = c.req.query('tag')
const repo = new DocumentRepository(c.env.DB, 'default')
const filters: Record<string, string> = { status: 'published' }
if (category) filters.category = category
if (tag) filters.tag = tag
const posts = await repo.list({
type: 'blog_posts',
filters,
limit,
offset: (page - 1) * limit,
})
return c.json({
success: true,
data: posts.items,
pagination: {
page,
limit,
total: posts.total
}
})
})
// Get single post by slug
blog.get('/posts/:slug', async (c) => {
const slug = c.req.param('slug')
const repo = new DocumentRepository(c.env.DB, 'default')
const results = await repo.list({
type: 'blog_posts',
filters: { slug, status: 'published' },
limit: 1,
})
const post = results.items[0]
if (!post) {
return c.json({ error: 'Post not found' }, 404)
}
return c.json({
success: true,
data: post
})
})
// Get posts by category
blog.get('/categories/:slug/posts', async (c) => {
const slug = c.req.param('slug')
const repo = new DocumentRepository(c.env.DB, 'default')
const posts = await repo.list({
type: 'blog_posts',
filters: { category: slug, status: 'published' },
})
return c.json({
success: true,
data: posts.items
})
})
export default blog
Frontend Integration
React Blog Component
// Blog listing component
export function BlogList() {
const [posts, setPosts] = useState([])
const [page, setPage] = useState(1)
useEffect(() => {
fetch(`/api/blog/posts?page=${page}&limit=10`)
.then(res => res.json())
.then(data => setPosts(data.data))
}, [page])
return (
<div className="blog-list">
{posts.map(post => (
<article key={post.id} className="blog-post">
<img src={post.featured_image} alt={post.title} />
<h2>{post.title}</h2>
<p className="excerpt">{post.excerpt}</p>
<div className="meta">
<span className="author">{post.author_name}</span>
<span className="date">{new Date(post.published_at).toLocaleDateString()}</span>
</div>
<a href={`/blog/${post.slug}`}>Read more →</a>
</article>
))}
<Pagination
currentPage={page}
onPageChange={setPage}
/>
</div>
)
}
// Single post component
export function BlogPost({ slug }) {
const [post, setPost] = useState(null)
useEffect(() => {
fetch(`/api/blog/posts/${slug}`)
.then(res => res.json())
.then(data => setPost(data.data))
}, [slug])
if (!post) return <div>Loading...</div>
return (
<article className="blog-post-single">
<header>
<h1>{post.title}</h1>
<div className="meta">
<img src={post.author_avatar} alt={post.author_name} />
<span>{post.author_name}</span>
<time>{new Date(post.published_at).toLocaleDateString()}</time>
</div>
</header>
{post.featured_image && (
<img src={post.featured_image} alt={post.title} className="featured" />
)}
<div
className="content"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<footer>
<div className="categories">
{post.categories?.split(',').map(cat => (
<a key={cat} href={`/blog/category/${cat}`}>{cat}</a>
))}
</div>
<div className="tags">
{post.tags?.split(',').map(tag => (
<a key={tag} href={`/blog/tag/${tag}`}>#{tag}</a>
))}
</div>
</footer>
</article>
)
}
E-commerce Catalog
Build a product catalog with categories, variants, and inventory management.
Collection Schema
Product Collections
// Products collection
{
id: 'products',
name: 'Products',
fields: [
{
name: 'name',
type: 'text',
required: true
},
{
name: 'slug',
type: 'text',
required: true,
unique: true
},
{
name: 'description',
type: 'richtext'
},
{
name: 'price',
type: 'number',
required: true,
validation: { min: 0 }
},
{
name: 'compareAtPrice',
type: 'number',
validation: { min: 0 }
},
{
name: 'images',
type: 'media',
multiple: true,
mediaTypes: ['image']
},
{
name: 'category',
type: 'relation',
collection: 'categories',
required: true
},
{
name: 'variants',
type: 'repeater',
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'sku', type: 'text', required: true, unique: true },
{ name: 'price', type: 'number', required: true },
{ name: 'stock', type: 'number', default: 0 },
{ name: 'attributes', type: 'json' }
]
},
{
name: 'inventory',
type: 'group',
fields: [
{ name: 'trackInventory', type: 'boolean', default: true },
{ name: 'stock', type: 'number', default: 0 },
{ name: 'lowStockThreshold', type: 'number', default: 10 }
]
},
{
name: 'seo',
type: 'group',
fields: [
{ name: 'metaTitle', type: 'text' },
{ name: 'metaDescription', type: 'textarea' }
]
},
{
name: 'status',
type: 'select',
options: ['active', 'draft', 'archived'],
default: 'draft'
}
]
}
API Implementation
Product API
import { Hono } from 'hono'
import { DocumentRepository } from '@sonicjs-cms/core'
const shop = new Hono()
// Get all products with filtering
// Equivalent public API: GET /api/products?status=active&category=<slug>
shop.get('/products', async (c) => {
const category = c.req.query('category')
const minPrice = c.req.query('minPrice')
const maxPrice = c.req.query('maxPrice')
const inStock = c.req.query('inStock') === 'true'
const repo = new DocumentRepository(c.env.DB, 'default')
const filters: Record<string, string> = { status: 'active' }
if (category) filters.category = category
if (minPrice) filters.minPrice = minPrice
if (maxPrice) filters.maxPrice = maxPrice
if (inStock) filters.inStock = 'true'
const products = await repo.list({ type: 'products', filters })
return c.json({
success: true,
data: products.items
})
})
// Get single product
shop.get('/products/:slug', async (c) => {
const slug = c.req.param('slug')
const repo = new DocumentRepository(c.env.DB, 'default')
const results = await repo.list({
type: 'products',
filters: { slug, status: 'active' },
limit: 1,
})
const product = results.items[0]
if (!product) {
return c.json({ error: 'Product not found' }, 404)
}
return c.json({
success: true,
data: product
})
})
// Check product availability
shop.get('/products/:slug/availability', async (c) => {
const slug = c.req.param('slug')
const variantId = c.req.query('variantId')
const repo = new DocumentRepository(c.env.DB, 'default')
const results = await repo.list({
type: 'products',
filters: { slug, status: 'active' },
limit: 1,
})
const product = results.items[0]
if (!product) {
return c.json({ error: 'Product not found' }, 404)
}
const variants: Array<{ id: string; stock: number }> = product.data?.variants ?? []
const variant = variants.find(v => v.id === variantId)
const stock = variant?.stock ?? product.data?.stock ?? 0
const available = stock > 0
return c.json({
success: true,
data: {
available,
stock,
lowStock: stock < (product.data?.low_stock_threshold ?? 10)
}
})
})
export default shop
Documentation Site
Create a searchable documentation site with versioning and navigation.
Collection Schema
Documentation Collections
// Documentation pages collection
{
id: 'docs',
name: 'Documentation',
fields: [
{
name: 'title',
type: 'text',
required: true
},
{
name: 'slug',
type: 'text',
required: true
},
{
name: 'content',
type: 'markdown',
required: true
},
{
name: 'excerpt',
type: 'textarea'
},
{
name: 'category',
type: 'relation',
collection: 'doc_categories',
required: true
},
{
name: 'order',
type: 'number',
default: 0
},
{
name: 'version',
type: 'text',
default: '1.0'
},
{
name: 'sections',
type: 'repeater',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'id', type: 'text', required: true }
]
},
{
name: 'relatedDocs',
type: 'relation',
collection: 'docs',
multiple: true
}
]
}
API with Search
Documentation API
import { Hono } from 'hono'
const docs = new Hono()
// Get documentation navigation
docs.get('/nav', async (c) => {
const version = c.req.query('version') || '1.0'
const db = c.env.DB
const categories = await db
.prepare(`
SELECT c.*,
(SELECT json_group_array(json_object('id', d.id, 'title', d.title, 'slug', d.slug, 'order', d.order))
FROM docs d
WHERE d.category_id = c.id AND d.version = ?
ORDER BY d.order ASC) as pages
FROM doc_categories c
ORDER BY c.order ASC
`)
.bind(version)
.all()
return c.json({
success: true,
data: categories.results.map(cat => ({
...cat,
pages: JSON.parse(cat.pages)
}))
})
})
// Get single documentation page
docs.get('/:slug', async (c) => {
const slug = c.req.param('slug')
const version = c.req.query('version') || '1.0'
const db = c.env.DB
const doc = await db
.prepare(`
SELECT d.*, c.name as category_name
FROM docs d
JOIN doc_categories c ON d.category_id = c.id
WHERE d.slug = ? AND d.version = ?
`)
.bind(slug, version)
.first()
if (!doc) {
return c.json({ error: 'Page not found' }, 404)
}
// Parse JSON fields
doc.sections = JSON.parse(doc.sections || '[]')
doc.related_docs = JSON.parse(doc.related_docs || '[]')
return c.json({
success: true,
data: doc
})
})
// Search documentation
docs.get('/search', async (c) => {
const query = c.req.query('q')
const version = c.req.query('version') || '1.0'
if (!query || query.length < 2) {
return c.json({
success: false,
error: 'Query must be at least 2 characters'
}, 400)
}
const db = c.env.DB
const results = await db
.prepare(`
SELECT d.id, d.title, d.slug, d.excerpt, c.name as category
FROM docs d
JOIN doc_categories c ON d.category_id = c.id
WHERE d.version = ?
AND (d.title LIKE ? OR d.content LIKE ? OR d.excerpt LIKE ?)
ORDER BY
CASE
WHEN d.title LIKE ? THEN 1
WHEN d.excerpt LIKE ? THEN 2
ELSE 3
END,
d.order ASC
LIMIT 20
`)
.bind(version, `%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`)
.all()
return c.json({
success: true,
data: results.results,
query
})
})
export default docs
Multi-tenant App
Build a multi-tenant SaaS application with isolated data per tenant.
Tenant Isolation Strategy
Tenant Middleware
import { Hono } from 'hono'
// Tenant identification middleware
export function tenantMiddleware() {
return async (c, next) => {
// Extract tenant from subdomain or header
const host = c.req.header('host') || ''
const subdomain = host.split('.')[0]
// Or from custom header
const tenantId = c.req.header('x-tenant-id') || subdomain
if (!tenantId) {
return c.json({ error: 'Tenant not specified' }, 400)
}
const db = c.env.DB
// Get tenant info
const tenant = await db
.prepare('SELECT * FROM tenants WHERE subdomain = ? AND active = 1')
.bind(tenantId)
.first()
if (!tenant) {
return c.json({ error: 'Tenant not found' }, 404)
}
// Add tenant to context
c.set('tenant', tenant)
await next()
}
}
// Tenant-scoped queries
export function withTenant(c) {
const tenant = c.get('tenant')
return {
// All queries automatically filter by tenant
async prepare(sql) {
const modifiedSql = sql.replace(
/FROM (\w+)/g,
`FROM $1 WHERE tenant_id = ${tenant.id}`
)
return c.env.DB.prepare(modifiedSql)
}
}
}
Multi-tenant API
Tenant API Routes
const app = new Hono()
// Apply tenant middleware globally
app.use('*', tenantMiddleware())
// Get tenant content
app.get('/content', async (c) => {
const tenant = c.get('tenant')
// DocumentRepository is scoped to tenant_id — passes tenant.id as tenantId
const repo = new DocumentRepository(c.env.DB, tenant.id)
const results = await repo.list({ filters: { status: 'published' } })
return c.json({
success: true,
data: results.items,
tenant: {
id: tenant.id,
name: tenant.name
}
})
})
// Create tenant content
app.post('/content', async (c) => {
const tenant = c.get('tenant')
const body = await c.req.json()
// DocumentsService.create() handles raw prepare/bind/batch (R1) scoped to tenant
const { DocumentsService } = await import('@sonicjs-cms/core')
const svc = new DocumentsService(c.env.DB, tenant.id)
const doc = await svc.create({
type: body.type || 'content',
data: body,
})
return c.json({
success: true,
data: doc
})
})
// Tenant settings
app.get('/settings', async (c) => {
const tenant = c.get('tenant')
return c.json({
success: true,
data: {
name: tenant.name,
subdomain: tenant.subdomain,
plan: tenant.plan,
features: JSON.parse(tenant.features || '[]'),
customization: JSON.parse(tenant.customization || '{}')
}
})
})
export default app
API-First CMS
Use SonicJS as a headless CMS for any frontend framework.
RESTful API Pattern
Content API
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { getCollectionRegistry, DocumentRepository } from '@sonicjs-cms/core'
const api = new Hono()
// Enable CORS for all origins
api.use('*', cors())
// Generic content endpoint
api.get('/:collection', async (c) => {
const collection = c.req.param('collection')
// Collections are code-defined in-memory — no DB table
const registry = getCollectionRegistry()
const collectionExists = registry.listActive().find(col => col.id === collection)
if (!collectionExists) {
return c.json({ error: 'Collection not found' }, 404)
}
// Get content with pagination using DocumentRepository
const page = parseInt(c.req.query('page') || '1')
const limit = Math.min(parseInt(c.req.query('limit') || '10'), 100)
const offset = (page - 1) * limit
const repo = new DocumentRepository(c.env.DB, 'default')
const results = await repo.list({
type: collection,
filters: { status: 'published' },
limit,
offset,
})
return c.json({
success: true,
data: results.items,
pagination: {
page,
limit,
total: results.total,
pages: Math.ceil(results.total / limit)
}
})
})
// Get single content item
api.get('/:collection/:id', async (c) => {
const collection = c.req.param('collection')
const id = c.req.param('id')
const repo = new DocumentRepository(c.env.DB, 'default')
// Try by id first, fall back to slug
const byId = await repo.list({
type: collection,
filters: { id, status: 'published' },
limit: 1,
})
const bySlug = byId.items.length === 0
? await repo.list({ type: collection, filters: { slug: id, status: 'published' }, limit: 1 })
: byId
const content = byId.items[0] ?? bySlug.items[0]
if (!content) {
return c.json({ error: 'Content not found' }, 404)
}
return c.json({
success: true,
data: content
})
})
export default api
Frontend Integration Examples
Framework Examples
// React with hooks
function useSonicJS(collection, id = null) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const url = id
? `/api/${collection}/${id}`
: `/api/${collection}`
fetch(url)
.then(res => res.json())
.then(result => {
setData(result.data)
setLoading(false)
})
}, [collection, id])
return { data, loading }
}
// Usage
function BlogPost({ slug }) {
const { data: post, loading } = useSonicJS('blog_posts', slug)
if (loading) return <div>Loading...</div>
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Custom Workflow
Implement a custom content approval workflow with multiple stages.
Workflow Plugin
Workflow Implementation
import { definePlugin, DocumentsService } from '@sonicjs-cms/core'
const workflow = definePlugin({
name: 'content-workflow',
version: '1.0.0',
description: 'Multi-stage content approval workflow'
})
// Workflow stages
const STAGES = {
DRAFT: 'draft',
REVIEW: 'review',
APPROVED: 'approved',
PUBLISHED: 'published',
REJECTED: 'rejected'
}
// Add workflow routes
const routes = new Hono()
// Submit content for review
routes.post('/submit/:contentId', async (c) => {
const contentId = c.req.param('contentId')
const user = c.get('user')
// Update document status via DocumentsService (raw prepare/bind/batch — R1)
const svc = new DocumentsService(c.env.DB, 'default')
await svc.saveDraft(contentId, {
status: STAGES.REVIEW,
submitted_by: user.userId,
submitted_at: Math.floor(Date.now() / 1000),
})
// Create workflow entry
await c.env.DB.prepare(`
INSERT INTO workflow_history
(content_id, from_stage, to_stage, user_id, created_at)
VALUES (?, ?, ?, ?, ?)
`)
.bind(contentId, STAGES.DRAFT, STAGES.REVIEW, user.userId, Math.floor(Date.now() / 1000))
.run()
return c.json({
success: true,
message: 'Content submitted for review'
})
})
// Approve content
routes.post('/approve/:contentId', async (c) => {
const contentId = c.req.param('contentId')
const user = c.get('user')
const { comments } = await c.req.json()
// Check permissions
if (user.role !== 'editor' && user.role !== 'admin') {
return c.json({ error: 'Insufficient permissions' }, 403)
}
const svc = new DocumentsService(c.env.DB, 'default')
await svc.saveDraft(contentId, {
status: STAGES.APPROVED,
approved_by: user.userId,
approved_at: Math.floor(Date.now() / 1000),
})
await c.env.DB.prepare(`
INSERT INTO workflow_history
(content_id, from_stage, to_stage, user_id, comments, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`)
.bind(contentId, STAGES.REVIEW, STAGES.APPROVED, user.userId, comments, Math.floor(Date.now() / 1000))
.run()
return c.json({
success: true,
message: 'Content approved'
})
})
// Reject content
routes.post('/reject/:contentId', async (c) => {
const contentId = c.req.param('contentId')
const user = c.get('user')
const { reason } = await c.req.json()
const svc = new DocumentsService(c.env.DB, 'default')
await svc.saveDraft(contentId, { status: STAGES.REJECTED })
await c.env.DB.prepare(`
INSERT INTO workflow_history
(content_id, from_stage, to_stage, user_id, comments, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`)
.bind(contentId, STAGES.REVIEW, STAGES.REJECTED, user.userId, reason, Math.floor(Date.now() / 1000))
.run()
return c.json({
success: true,
message: 'Content rejected'
})
})
// Get workflow history
routes.get('/history/:contentId', async (c) => {
const contentId = c.req.param('contentId')
const history = await c.env.DB
.prepare(`
SELECT wh.*, u.name as user_name
FROM workflow_history wh
JOIN users u ON wh.user_id = u.id
WHERE wh.content_id = ?
ORDER BY wh.created_at DESC
`)
.bind(contentId)
.all()
return c.json({
success: true,
data: history.results
})
})
workflow.addRoute('/api/workflow', routes)
export default workflow