Coding Standards
Recommended patterns and practices for building ShellApps.
Project Structure
my-shellapp/
├── app/
│ ├── layout.tsx # Root layout with providers
│ ├── page.tsx # Home page
│ ├── providers.tsx # Client-side providers
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── callback/page.tsx
│ ├── dashboard/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── api/
│ └── webhooks/
│ └── route.ts
├── components/
│ ├── ui/ # App-specific UI components
│ ├── forms/ # Form components
│ └── layouts/ # Layout components
├── lib/
│ ├── api.ts # API client helpers
│ ├── utils.ts # Utility functions
│ └── constants.ts # App constants
├── hooks/ # Custom React hooks
├── types/ # TypeScript type definitions
└── public/ # Static assetsTypeScript Usage
Always use strict TypeScript. Enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}Type Patterns
// Use interfaces for objects, types for unions/intersections
interface User {
id: string
displayName: string
email: string
profiles: Profile[]
}
type AuthState = 'loading' | 'authenticated' | 'unauthenticated'
// Avoid `any` — use `unknown` and narrow
function processInput(input: unknown): string {
if (typeof input === 'string') return input
if (typeof input === 'number') return String(input)
throw new Error('Unexpected input type')
}Component Patterns with react-ui
Composition over Configuration
import { Card, Button, Stack, Text } from '@shellapps/react-ui'
// Good — composable
function FeatureCard({ title, description, onAction }: FeatureCardProps) {
return (
<Card>
<Stack gap="sm">
<Text as="h3" weight="bold">{title}</Text>
<Text color="muted">{description}</Text>
<Button onClick={onAction} variant="primary">
Learn More
</Button>
</Stack>
</Card>
)
}Client vs Server Components
// Server Component (default) — no 'use client' needed
async function UserList() {
const users = await fetchUsers()
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
// Client Component — only when you need interactivity
'use client'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}Error Handling
Use ErrorBoundary from react-ui
import { ErrorBoundary } from '@shellapps/react-ui'
function App() {
return (
<ErrorBoundary
fallback={({ error, reset }) => (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<Dashboard />
</ErrorBoundary>
)
}API Error Handling
import { ApiError } from '@shellapps/auth-sdk'
async function fetchData() {
try {
const res = await api.get('/data')
return res.data
} catch (error) {
if (error instanceof ApiError) {
if (error.status === 401) redirect('/login')
if (error.status === 429) throw new Error('Rate limited. Try again later.')
}
throw error
}
}Testing
Unit Tests
__tests__/utils.test.ts
import { describe, it, expect } from 'vitest'
import { formatCurrency } from '@/lib/utils'
describe('formatCurrency', () => {
it('formats USD correctly', () => {
expect(formatCurrency(1234.5, 'USD')).toBe('$1,234.50')
})
})Component Tests
__tests__/auth-buttons.test.tsx
import { render, screen } from '@testing-library/react'
import { AuthButtons } from '@/components/auth-buttons'
it('shows login button when not authenticated', () => {
render(<AuthButtons />)
expect(screen.getByText('Log In')).toBeInTheDocument()
})Recommended Setup
npm install -D vitest @testing-library/react @testing-library/jest-domPerformance Optimization
- Use Server Components by default — only add
'use client'when needed - Lazy-load heavy components with
next/dynamic - Optimize images with
next/image - Minimize client-side JavaScript — keep bundles small
- Use
data-tattributes for tracking instead of custom event listeners
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/chart'), {
loading: () => <div>Loading chart...</div>,
ssr: false,
})Accessibility
- Use semantic HTML (
nav,main,article,button) - Ensure all interactive elements are keyboard-accessible
- Provide
aria-labelfor icon-only buttons - Maintain colour contrast ratios (WCAG 2.1 AA minimum)
- Test with screen readers
// Good
<button aria-label="Close dialog" onClick={onClose}>
<CloseIcon />
</button>
// Good — react-ui components handle a11y by default
<Button variant="primary">Submit</Button>Next Steps
- Quick Start — Build your first ShellApp
- Authentication — Add login and signup
- Deployment — Deploy to production