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
    }
  ]
}

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

Next Steps

Was this page helpful?