Coding Standards

Consistent coding standards ensure a maintainable codebase and make it easier for everyone to contribute.

๐Ÿ“

Naming Conventions

Clear rules for naming collections, fields, variables, and files

๐Ÿ”„

Consistency

camelCase for JavaScript, snake_case for database/collections

โœจ

TypeScript First

All code must be properly typed with TypeScript

๐Ÿงช

Test Coverage

New features must include comprehensive tests


Naming Conventions

SonicJS follows industry-standard naming conventions with clear boundaries between JavaScript/TypeScript code and database layers.

Quick Reference

ElementConventionExample
Collection namessnake_caseblog_posts, user_profiles
Schema field namescamelCasefirstName, createdAt
Database columnssnake_casefirst_name, created_at
API responsescamelCaseuserId, createdAt
TypeScript variablescamelCaseuserData, isActive
TypeScript functionscamelCasegetUserById, validateInput
Classes & TypesPascalCaseUserService, BlogPost
ConstantsSCREAMING_SNAKE_CASEMAX_RETRIES, API_BASE_URL
File nameskebab-caseuser-service.ts

Collection Names

Collection names must be lowercase with underscores (snake_case). This is enforced by validation.

Collection Names

// All lowercase with underscores
name: 'blog_posts'
name: 'user_profiles'
name: 'order_items'

Schema Field Names

Schema field names should use camelCase in the TypeScript definition:

Schema Fields

export const blogPostsCollection: CollectionConfig = {
  name: 'blog_posts',
  schema: {
    type: 'object',
    properties: {
      title: { type: 'string' },
      featuredImage: { type: 'string', format: 'media' },
      publishedAt: { type: 'string', format: 'date-time' },
      metaDescription: { type: 'string' },
    },
  },
}

Database Columns

Database column names use snake_case (SQL convention). Drizzle ORM maps camelCase TypeScript properties to snake_case columns:

// TypeScript property -> SQL column
firstName: text('first_name').notNull(),
lastName: text('last_name').notNull(),
createdAt: integer('created_at'),
updatedAt: integer('updated_at'),
isActive: integer('is_active', { mode: 'boolean' }),

API Responses

API responses should use camelCase (JavaScript convention). Transform database results at the API boundary:

API Responses

{
  "id": "123",
  "userId": "user_456",
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T10:30:00Z",
  "firstName": "John",
  "lastName": "Doe"
}

TypeScript Conventions

Variables and Functions

Use camelCase for variables and functions:

// Correct
const userData = await getUserById(userId)
const isValidEmail = validateEmail(email)
let itemCount = 0

// Incorrect
const user_data = await get_user_by_id(user_id)  // Wrong
const IsValidEmail = validateEmail(email)         // Wrong

Classes, Types, and Interfaces

Use PascalCase for classes, types, and interfaces:

// Correct
class UserService { }
type BlogPost = { title: string; content: string }
interface ApiResponse<T> { data: T; status: number }

// Incorrect
class userService { }        // Wrong
type blogPost = { }          // Wrong

Constants

Use SCREAMING_SNAKE_CASE for constants:

// Correct
const MAX_RETRY_ATTEMPTS = 3
const API_BASE_URL = 'https://api.example.com'
const DEFAULT_PAGE_SIZE = 20

// Incorrect
const maxRetryAttempts = 3   // Looks like a variable

File Names

Use kebab-case for file names:

// Correct
user-service.ts
blog-posts.collection.ts
api-response.types.ts
auth.middleware.ts

// Incorrect
userService.ts              // camelCase
BlogPosts.collection.ts     // PascalCase
user_service.ts             // snake_case

Code Style

TypeScript Requirements

  • Always use TypeScript with proper type annotations
  • Avoid any type - use unknown if the type is truly unknown
  • Prefer interfaces over type aliases for object shapes
// Correct
function getUser(id: string): Promise<User | null> {
  // ...
}

// Incorrect
function getUser(id): any {  // Missing types
  // ...
}

Async/Await

Prefer async/await over .then() chains:

Async Patterns

async function fetchData() {
  try {
    const result = await api.getData()
    return result
  } catch (error) {
    logger.error('Failed to fetch data', error)
    throw error
  }
}

Import Organization

Organize imports in this order:

  1. Node.js built-in modules
  2. External dependencies
  3. Internal modules (absolute paths)
  4. Relative imports
// 1. Built-in modules
import { readFile } from 'fs/promises'

// 2. External dependencies
import { Hono } from 'hono'
import { drizzle } from 'drizzle-orm/d1'

// 3. Internal modules
import { UserService } from '@/services/user-service'
import { validateInput } from '@/utils/validation'

// 4. Relative imports
import { localHelper } from './helpers'
import type { LocalType } from './types'

Project Patterns

Collection Definitions

Collection Pattern

import { CollectionConfig } from '@sonicjs/core'

export const blogPostsCollection: CollectionConfig = {
  name: 'blog_posts',  // snake_case
  schema: {
    type: 'object',
    properties: {
      title: {
        type: 'string',
        title: 'Title',
        required: true,
      },
      content: {
        type: 'string',
        title: 'Content',
        format: 'richtext',
      },
      featuredImage: {  // camelCase
        type: 'string',
        title: 'Featured Image',
        format: 'media',
      },
      publishedAt: {  // camelCase
        type: 'string',
        title: 'Published At',
        format: 'date-time',
      },
    },
  },
  access: {
    read: () => true,
    create: ({ user }) => !!user,
    update: ({ user }) => !!user,
    delete: ({ user }) => user?.role === 'admin',
  },
}

Route Handlers

Route Handler Pattern

import { Hono } from 'hono'
import type { Context } from 'hono'

const app = new Hono()

app.get('/users/:id', async (c: Context) => {
  const { id } = c.req.param()

  try {
    const user = await userService.getById(id)

    if (!user) {
      return c.json({ error: 'User not found' }, 404)
    }

    return c.json({ data: user })  // camelCase in response
  } catch (error) {
    logger.error('Failed to get user', { id, error })
    return c.json({ error: 'Internal server error' }, 500)
  }
})

Middleware

Middleware Pattern

import type { MiddlewareHandler } from 'hono'

export const authMiddleware: MiddlewareHandler = async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')

  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  try {
    const user = await verifyToken(token)
    c.set('user', user)
    await next()
  } catch (error) {
    return c.json({ error: 'Invalid token' }, 401)
  }
}

Testing Standards

Test File Location

Place test files next to the code they test:

src/
  services/
    user-service.ts
    user-service.test.ts
  routes/
    api.ts
    api.test.ts

Test Naming

Use descriptive test names:

describe('UserService', () => {
  describe('getById', () => {
    it('returns user when found', async () => {
      // ...
    })

    it('returns null when user does not exist', async () => {
      // ...
    })

    it('throws error when database connection fails', async () => {
      // ...
    })
  })
})

Formatting and Linting

Automatic Formatting

Prettier runs automatically on commit with this configuration:

{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}

Before Committing

Run these commands before submitting a PR:

npm run lint      # ESLint rules must pass
npm run format    # Format with Prettier
npm test          # All tests must pass

Questions?

If you have questions about these standards:

Was this page helpful?