Skip to content

Code Conventions

This guide establishes coding standards and conventions for maintaining a consistent, readable, and maintainable codebase.

  • Use kebab-case for all file and directory names
  • Use descriptive names that clearly indicate purpose
  • Avoid abbreviations unless they’re widely understood
src/components/
├── newsletter-form/
│ ├── index.ts # Barrel export
│ ├── newsletter-form.tsx # Main component
│ ├── newsletter-form.stories.tsx # Storybook stories
│ └── types.ts # Component-specific types
src/pages/
├── articles/
│ ├── [slug]/
│ │ ├── index.astro # Main page component
│ │ ├── _components/ # Page-specific components
│ │ └── _graphql/ # Page-specific data fetching
│ └── index.astro # Article listing page
src/utils/
├── format-date/
│ ├── index.ts # Main utility function
│ ├── format-date.spec.ts # Tests
│ └── types.ts # Type definitions

Astro Components (.astro):

---
// 1. Type imports
import type { ComponentProps } from "astro/types";
// 2. Component imports
import { BaseComponent } from "#components/base";
// 3. Utility imports
import { formatDate } from "#utils/format-date";
// 4. Interface definition
interface Props {
title: string;
date?: Date;
className?: string;
}
// 5. Props destructuring with defaults
const { title, date = new Date(), className = "", ...rest } = Astro.props;
// 6. Data processing
const formattedDate = formatDate(date);
---
<!-- 7. Template with semantic HTML -->
<article class:list={["article", className]} {...rest}>
<h1 class="article__title">{title}</h1>
<time class="article__date">{formattedDate}</time>
<slot />
</article>

SolidJS Components (.tsx):

// 1. Framework imports
import { Component, createSignal, createMemo } from 'solid-js';
// 2. Type imports
import type { JSX } from 'solid-js';
// 3. Utility imports
import { cn } from '#utils/classnames';
// 4. Interface definition
interface Props {
initialValue?: string;
onSubmit: (value: string) => void;
className?: string;
}
// 5. Component definition
export const InteractiveForm: Component<Props> = (props) => {
// 6. Signals and state
const [value, setValue] = createSignal(props.initialValue ?? '');
const [loading, setLoading] = createSignal(false);
// 7. Computed values
const isValid = createMemo(() => value().length > 0);
// 8. Event handlers
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!isValid()) return;
setLoading(true);
try {
await props.onSubmit(value());
} catch (error) {
console.error('Form submission failed:', error);
} finally {
setLoading(false);
}
};
// 9. JSX return
return (
<form
onSubmit={handleSubmit}
class={cn('form', props.className)}
>
<input
type="text"
value={value()}
onInput={(e) => setValue(e.currentTarget.value)}
disabled={loading()}
class="form__input"
/>
<button
type="submit"
disabled={!isValid() || loading()}
class="form__submit"
>
{loading() ? 'Submitting...' : 'Submit'}
</button>
</form>
);
};
// Use camelCase for variables and functions
const userName = "john_doe";
const isAuthenticated = true;
const getUserProfile = async (id: string) => {
/* ... */
};
// Use descriptive names
const fetchUserArticles = async (userId: string) => {
/* ... */
}; // ✅
const getData = async (id: string) => {
/* ... */
}; // ❌
// Boolean variables should be questions
const isLoading = true; // ✅
const hasErrors = false; // ✅
const loading = true; // ❌ (not descriptive enough)
// Use SCREAMING_SNAKE_CASE for constants
const API_BASE_URL = "https://api.example.com";
const MAX_RETRY_ATTEMPTS = 3;
const DEFAULT_TIMEOUT = 5000;
// Group related constants
const VALIDATION_RULES = {
MIN_PASSWORD_LENGTH: 8,
MAX_USERNAME_LENGTH: 50,
ALLOWED_EMAIL_DOMAINS: ["company.com", "contractor.com"],
} as const;
// Use PascalCase for types and interfaces
interface UserProfile {
id: string;
name: string;
email: string;
}
type ArticleStatus = "draft" | "published" | "archived";
// Prefix interfaces with 'I' only when needed for disambiguation
interface Props {
title: string;
}
interface IUserService {
// Only when distinguishing from UserService class
getUser(id: string): Promise<User>;
}
/* Use BEM methodology for component-specific styles */
.article {
/* Block */
}
.article__title {
/* Element */
}
.article__title--large {
/* Modifier */
}
/* Use Tailwind utilities for most styling */
<div class="bg-primary text-secondary p-4 rounded-lg">
// 1. Node modules
import { Component } from "solid-js";
import type { JSX } from "solid-js";
// 2. Internal modules (with path aliases)
import { Button } from "#components/button";
import { formatDate } from "#utils/format-date";
import type { User } from "#types/user";
// 3. Relative imports
import "./component.css";
import { localHelper } from "./utils";
// Use configured path aliases
import { Button } from "#components/button"; // ✅
import { formatDate } from "#utils/format-date"; // ✅
import { Button } from "../../components/button"; // ❌
// Use structured error handling with Airbrake
try {
const result = await apiCall();
return result;
} catch (error) {
// Log for debugging
console.error("API call failed:", {
error: error.message,
component: "ComponentName",
context: { userId: props.userId },
});
// Report to monitoring
const { airbrakeClient } = await import("#utils/airbrake/browser");
airbrakeClient.notify({
error,
context: {
component: "ComponentName",
userId: props.userId,
},
});
// Return user-friendly fallback
return { error: "Unable to load data. Please try again." };
}
// Return appropriate HTTP status codes
export async function GET({ params, request }) {
try {
const data = await fetchData(params.id);
if (!data) {
return new Response(JSON.stringify({ error: "Not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Server error:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
/**
* Interactive form component for user input with validation
*
* @example
* ```tsx
* <InteractiveForm
* initialValue="Hello"
* onSubmit={(value) => console.log(value)}
* className="my-form"
* />
* ```
*/
interface Props {
/** Initial value for the form input */
initialValue?: string;
/** Callback function called when form is submitted */
onSubmit: (value: string) => void;
/** Additional CSS classes to apply */
className?: string;
}
/**
* Formats a date according to the application's locale settings
*
* @param date - The date to format
* @param options - Formatting options
* @returns Formatted date string
*
* @example
* ```typescript
* const formatted = formatDate(new Date(), { includeTime: true });
* // Returns: "March 15, 2024 at 2:30 PM"
* ```
*/
export function formatDate(date: Date, options: FormatOptions = {}): string {
// Implementation
}
Component.test.tsx
import { render, screen } from '@testing-library/solid';
import { Component } from './Component';
describe('Component', () => {
it('should render with default props', () => {
render(() => <Component title="Test" />);
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('should handle user interaction', async () => {
const handleClick = vi.fn();
render(() => <Component title="Test" onClick={handleClick} />);
const button = screen.getByRole('button');
await user.click(button);
expect(handleClick).toHaveBeenCalledOnce();
});
});
  • Use descriptive test names that explain the expected behavior
  • Group related tests using describe blocks
  • Use “should” statements for test descriptions
// Use createMemo for expensive computations
const expensiveValue = createMemo(() => {
return heavyComputation(props.data);
});
// Avoid creating objects in JSX
const styles = { color: 'red' }; // ✅ Define outside render
return <div style={styles}>Content</div>;
// Not this:
return <div style={{ color: 'red' }}>Content</div>; // ❌ Creates new object each render
// Use dynamic imports for large dependencies
const heavyLibrary = await import("heavy-library");
// Tree shake unused exports
import { specificFunction } from "large-library"; // ✅
import * as library from "large-library"; // ❌ Imports everything
<!-- Use proper semantic elements -->
<main>
<article>
<header>
<h1>Article Title</h1>
<time datetime="2024-03-15">March 15, 2024</time>
</header>
<p>Article content...</p>
</article>
</main>
// Provide proper labels and descriptions
<button
aria-label="Close dialog"
aria-describedby="close-help"
onClick={handleClose}
>
×
</button>
<div id="close-help" class="sr-only">
Closes the current dialog and returns to the main content
</div>
  • Follow the configured ESLint rules in .eslintrc.js
  • Use npm run lint to check for violations
  • Fix auto-fixable issues with npm run lint:fix
  • Code formatting is handled automatically by Prettier
  • Configuration is in .prettierrc
  • VS Code should format on save if configured properly
Terminal window
# Format: type(scope): description
feat(components): add newsletter signup component
fix(api): handle null responses in user endpoint
docs(readme): update installation instructions
refactor(utils): simplify date formatting logic
Terminal window
# Format: {initials}-{ticket-number|description}
jd-lpweb-1234-add-user-profile
sm-fix-navigation-bug
ak-update-design-tokens