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'
const blog = new Hono()
// Get all published posts with pagination
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 db = c.env.DB
let query = `
SELECT p.*, a.name as author_name, a.avatar as author_avatar
FROM blog_posts p
JOIN authors a ON p.author_id = a.id
WHERE p.status = 'published'
AND p.published_at <= datetime('now')
`
if (category) {
query += ` AND p.categories LIKE '%${category}%'`
}
if (tag) {
query += ` AND p.tags LIKE '%${tag}%'`
}
query += ` ORDER BY p.published_at DESC LIMIT ${limit} OFFSET ${(page - 1) * limit}`
const posts = await db.prepare(query).all()
return c.json({
success: true,
data: posts.results,
pagination: {
page,
limit,
total: posts.results.length
}
})
})
// Get single post by slug
blog.get('/posts/:slug', async (c) => {
const slug = c.req.param('slug')
const db = c.env.DB
const post = await db
.prepare(`
SELECT p.*, a.name as author_name, a.bio as author_bio
FROM blog_posts p
JOIN authors a ON p.author_id = a.id
WHERE p.slug = ? AND p.status = 'published'
`)
.bind(slug)
.first()
if (!post) {
return c.json({ error: 'Post not found' }, 404)
}
// Increment view count
await db
.prepare('UPDATE blog_posts SET views = views + 1 WHERE id = ?')
.bind(post.id)
.run()
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 db = c.env.DB
const posts = await db
.prepare(`
SELECT p.* FROM blog_posts p
WHERE p.categories LIKE '%' || ? || '%'
AND p.status = 'published'
ORDER BY p.published_at DESC
`)
.bind(slug)
.all()
return c.json({
success: true,
data: posts.results
})
})
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'
const shop = new Hono()
// Get all products with filtering
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 sort = c.req.query('sort') || 'name_asc'
const db = c.env.DB
let query = `
SELECT p.*, c.name as category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.status = 'active'
`
const params = []
if (category) {
query += ` AND c.slug = ?`
params.push(category)
}
if (minPrice) {
query += ` AND p.price >= ?`
params.push(parseFloat(minPrice))
}
if (maxPrice) {
query += ` AND p.price <= ?`
params.push(parseFloat(maxPrice))
}
if (inStock) {
query += ` AND p.stock > 0`
}
// Sorting
const [field, direction] = sort.split('_')
query += ` ORDER BY p.${field} ${direction.toUpperCase()}`
const products = await db.prepare(query).bind(...params).all()
return c.json({
success: true,
data: products.results
})
})
// Get single product
shop.get('/products/:slug', async (c) => {
const slug = c.req.param('slug')
const db = c.env.DB
const product = await db
.prepare(`
SELECT p.*, c.name as category_name, c.slug as category_slug
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.slug = ? AND p.status = 'active'
`)
.bind(slug)
.first()
if (!product) {
return c.json({ error: 'Product not found' }, 404)
}
// Parse JSON fields
product.variants = JSON.parse(product.variants || '[]')
product.images = JSON.parse(product.images || '[]')
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 db = c.env.DB
const product = await db
.prepare('SELECT * FROM products WHERE slug = ?')
.bind(slug)
.first()
if (!product) {
return c.json({ error: 'Product not found' }, 404)
}
const variants = JSON.parse(product.variants || '[]')
const variant = variants.find(v => v.id === variantId)
const available = variant
? variant.stock > 0
: product.stock > 0
return c.json({
success: true,
data: {
available,
stock: variant?.stock || product.stock,
lowStock: (variant?.stock || product.stock) < product.low_stock_threshold
}
})
})
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')
const db = c.env.DB
const content = await db
.prepare('SELECT * FROM content WHERE tenant_id = ? ORDER BY created_at DESC')
.bind(tenant.id)
.all()
return c.json({
success: true,
data: content.results,
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()
const db = c.env.DB
const result = await db
.prepare(`
INSERT INTO content (tenant_id, title, body, created_at)
VALUES (?, ?, ?, ?)
`)
.bind(tenant.id, body.title, body.body, Date.now())
.run()
return c.json({
success: true,
data: {
id: result.lastInsertRowid,
...body,
tenant_id: tenant.id
}
})
})
// 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'
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')
const db = c.env.DB
// Validate collection exists
const collectionExists = await db
.prepare('SELECT * FROM collections WHERE id = ?')
.bind(collection)
.first()
if (!collectionExists) {
return c.json({ error: 'Collection not found' }, 404)
}
// Get content with pagination
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 content = await db
.prepare(`
SELECT * FROM content
WHERE collection = ? AND status = 'published'
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`)
.bind(collection, limit, offset)
.all()
const total = await db
.prepare('SELECT COUNT(*) as count FROM content WHERE collection = ? AND status = "published"')
.bind(collection)
.first()
return c.json({
success: true,
data: content.results.map(item => ({
...item,
data: JSON.parse(item.data)
})),
pagination: {
page,
limit,
total: total.count,
pages: Math.ceil(total.count / limit)
}
})
})
// Get single content item
api.get('/:collection/:id', async (c) => {
const collection = c.req.param('collection')
const id = c.req.param('id')
const db = c.env.DB
const content = await db
.prepare(`
SELECT * FROM content
WHERE collection = ? AND (id = ? OR slug = ?) AND status = 'published'
`)
.bind(collection, id, id)
.first()
if (!content) {
return c.json({ error: 'Content not found' }, 404)
}
return c.json({
success: true,
data: {
...content,
data: JSON.parse(content.data)
}
})
})
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 { PluginBuilder } from '@sonicjs-cms/core'
const workflow = PluginBuilder.create({
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')
const db = c.env.DB
// Update content status
await db
.prepare(`
UPDATE content
SET status = ?, submitted_by = ?, submitted_at = ?
WHERE id = ?
`)
.bind(STAGES.REVIEW, user.userId, Date.now(), contentId)
.run()
// Create workflow entry
await 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, Date.now())
.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()
const db = c.env.DB
// Check permissions
if (!user.role === 'editor' && !user.role === 'admin') {
return c.json({ error: 'Insufficient permissions' }, 403)
}
await db
.prepare(`
UPDATE content
SET status = ?, approved_by = ?, approved_at = ?
WHERE id = ?
`)
.bind(STAGES.APPROVED, user.userId, Date.now(), contentId)
.run()
await 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, Date.now())
.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 db = c.env.DB
await db
.prepare(`
UPDATE content
SET status = ?
WHERE id = ?
`)
.bind(STAGES.REJECTED, contentId)
.run()
await 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, Date.now())
.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 db = c.env.DB
const history = await 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.build()