Guides/Best Practices

Best Practices

Get the most out of Craft with these proven best practices. These tips help you build better applications faster.

AI Prompting#

Be Specific#

The more detail you provide, the better the results:

❌ Vague: "Make a form"

✅ Specific: "Create a contact form with name, email,
   and message fields. Add validation that requires
   all fields, validates email format, and shows
   inline error messages. Include a submit button
   that shows loading state while submitting."

Provide Context#

Help the AI understand your project:

"I'm building a project management app. Create a
TaskCard component that shows the task title,
assignee avatar, due date, and priority badge.
It should match the existing design system using
our Card and Badge components."

Iterate Incrementally#

Break complex features into steps:

Step 1: "Create the basic form structure with fields"
Step 2: "Add validation using Zod schema"
Step 3: "Add loading and success states"
Step 4: "Add error handling and display"
Step 5: "Polish animations and responsive design"

Reference Existing Patterns#

Point to code that works well:

"Create a DeleteConfirmationModal following the
same pattern as the existing PaymentModal component"

Code Organization#

File Structure#

Keep a consistent structure:

src/
├── app/              # Routes and pages
│   └── (group)/      # Route groups
├── components/
│   ├── ui/           # Reusable UI components
│   ├── forms/        # Form components
│   └── [feature]/    # Feature-specific components
├── lib/
│   ├── utils.ts      # Utility functions
│   ├── constants.ts  # App constants
│   └── types.ts      # TypeScript types
├── hooks/            # Custom React hooks
└── styles/           # Global styles

Naming Conventions#

Be consistent:

| Type | Convention | Example | | ---------- | -------------------- | -------------------- | | Components | PascalCase | UserProfile.tsx | | Utilities | camelCase | formatDate.ts | | Types | PascalCase | User, TaskStatus | | Constants | SCREAMING_SNAKE | API_URL | | Hooks | camelCase with use | useAuth.ts |

Component Organization#

Structure components predictably:

// 1. Imports
import { useState } from "react";
import { Button } from "@/components/ui/Button";

// 2. Types
interface Props {
  user: User;
  onSave: (user: User) => void;
}

// 3. Component
export function UserProfile({ user, onSave }: Props) {
  // 3a. Hooks
  const [isEditing, setIsEditing] = useState(false);

  // 3b. Event handlers
  const handleSave = () => {
    // ...
  };

  // 3c. Render
  return <div>{/* Component JSX */}</div>;
}

Performance#

Lazy Loading#

Load components only when needed:

import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("./HeavyChart"), {
  loading: () => <ChartSkeleton />,
});

Image Optimization#

Use Next.js Image component:

import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={600}
  priority // For above-the-fold images
/>;

Minimize Re-renders#

Use memoization wisely:

// Memoize expensive computations
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(data);
}, [data]);

// Memoize callbacks
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

// Memoize components
const MemoizedList = memo(function List({ items }) {
  return items.map((item) => <Item key={item.id} {...item} />);
});

TypeScript#

Use Types Liberally#

Type everything:

interface Task {
  id: string;
  title: string;
  status: "todo" | "in-progress" | "done";
  priority: "low" | "medium" | "high";
  dueDate?: Date;
  assignee?: User;
}

function updateTask(id: string, updates: Partial<Task>): Promise<Task> {
  // Implementation
}

Avoid any

Use proper types:

// ❌ Bad
function processData(data: any) {
  return data.map((item: any) => item.value);
}

// ✅ Good
interface DataItem {
  id: string;
  value: number;
}

function processData(data: DataItem[]): number[] {
  return data.map((item) => item.value);
}

Use Utility Types#

Leverage TypeScript's built-in utilities:

// Partial - all properties optional
type TaskUpdate = Partial<Task>;

// Pick - select specific properties
type TaskPreview = Pick<Task, "id" | "title" | "status">;

// Omit - exclude specific properties
type NewTask = Omit<Task, "id" | "createdAt">;

// Record - key-value mapping
type TaskMap = Record<string, Task>;

Error Handling#

Handle All Cases#

Plan for failures:

async function fetchData() {
  try {
    const response = await fetch("/api/data");

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      throw new Error("Network error");
    }
    throw error;
  }
}

User-Friendly Errors#

Show helpful messages:

function ErrorDisplay({ error }: { error: Error }) {
  return (
    <div className="p-4 bg-red-50 rounded-lg">
      <h3 className="font-medium text-red-800">Something went wrong</h3>
      <p className="text-sm text-red-600 mt-1">{getErrorMessage(error)}</p>
      <button onClick={retry} className="mt-3 text-sm underline">
        Try again
      </button>
    </div>
  );
}

Error Boundaries#

Contain failures:

"use client";

import { ErrorBoundary } from "react-error-boundary";

function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <Dashboard />
    </ErrorBoundary>
  );
}

Accessibility#

Semantic HTML#

Use proper elements:

// ❌ Bad
<div onClick={handleClick}>Click me</div>

// ✅ Good
<button onClick={handleClick}>Click me</button>

ARIA Labels#

Add context for screen readers:

<button aria-label="Delete task" onClick={handleDelete}>
  <TrashIcon />
</button>

Keyboard Navigation#

Ensure keyboard accessibility:

<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      handleClick();
    }
  }}
>
  Custom button
</div>

Testing#

Test User Flows#

Focus on user experience:

test("user can create a task", async () => {
  render(<TaskForm />);

  await userEvent.type(screen.getByLabelText("Title"), "New task");
  await userEvent.click(screen.getByRole("button", { name: "Create" }));

  expect(screen.getByText("Task created")).toBeInTheDocument();
});

Test Edge Cases#

Cover unusual scenarios:

test("handles empty input", async () => {
  render(<SearchInput />);

  await userEvent.type(screen.getByRole("textbox"), "   ");
  await userEvent.click(screen.getByRole("button", { name: "Search" }));

  expect(screen.getByText("Please enter a search term")).toBeInTheDocument();
});

Security#

Validate Input#

Never trust user input:

import { z } from "zod";

const TaskSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().max(1000).optional(),
  priority: z.enum(["low", "medium", "high"]),
});

export async function POST(request: Request) {
  const body = await request.json();
  const validated = TaskSchema.parse(body); // Throws on invalid
  // Process validated data
}

Protect API Routes#

Always verify authentication:

export async function GET(request: Request) {
  const session = await getSession();

  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Proceed with authenticated request
}

Review Checklist#

Before deploying, verify:

  • [ ] All forms have validation
  • [ ] Errors are handled gracefully
  • [ ] Loading states are implemented
  • [ ] Mobile responsive design works
  • [ ] Accessibility requirements met
  • [ ] No console errors
  • [ ] Performance is acceptable
  • [ ] Security best practices followed

Next Steps#