Best Practices & Code Patterns¶
Breadcrumbs: Documentation > Guides > Best Practices
This guide outlines recommended code patterns, conventions, and best practices for the portfolio website codebase.
Table of Contents¶
- Project Structure
- TypeScript Patterns
- React Component Patterns
- Data Management
- Styling Patterns
- Performance Optimization
- SEO Best Practices
- Security Best Practices
- Testing Patterns
Project Structure¶
File Organization¶
Follow these patterns for consistent file organization:
app/
├── (pages)/ # Page routes
│ ├── page.tsx # Home page
│ ├── layout.tsx # Root layout
│ └── projects/
│ ├── page.tsx # Projects list
│ └── [slug]/
│ └── page.tsx # Project detail
├── api/ # API routes (if needed)
├── globals.css # Global styles
├── sitemap.ts # Dynamic sitemap
└── robots.ts # Dynamic robots.txt
components/
├── ui/ # shadcn/ui components
│ ├── button.tsx
│ ├── card.tsx
│ └── ...
├── layout/ # Layout components
│ ├── header.tsx
│ ├── footer.tsx
│ └── navigation.tsx
├── meta/ # SEO/metadata components
│ ├── analytics.tsx
│ └── structured-data.tsx
└── [feature]/ # Feature-specific components
├── project-card.tsx
└── project-gallery.tsx
lib/
├── config.ts # Configuration
├── utils.ts # Utility functions
├── projects.ts # Project data access
├── achievements.ts # Achievement data access
└── skills.ts # Skill generation
content/
├── projects/ # Project JSON files
│ ├── project-1.json
│ └── project-2.json
└── achievements/ # Achievement JSON files
└── achievement-1.json
Import Organization¶
Order imports consistently:
// 1. External dependencies
import { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
// 2. Internal components (UI first)
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
// 3. Internal utilities and data
import { cn } from "@/lib/utils";
import { getProjects } from "@/lib/projects";
import { siteConfig } from "@/lib/config";
// 4. Types
import type { Project } from "@/lib/projects";
// 5. Styles (if any)
import styles from "./styles.module.css";
Always use absolute imports:
// ❌ BAD: Relative imports
import { Button } from "../../../components/ui/button";
import { cn } from "../../lib/utils";
// ✅ GOOD: Absolute imports
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
TypeScript Patterns¶
Interface Definitions¶
Define strict interfaces for all data structures:
// ✅ GOOD: Comprehensive interface
export interface Project {
slug: string;
title: string;
shortDescription: string;
description: string;
technologies: string[];
images: ProjectImage[];
demoUrl?: string; // Optional with ?
githubUrl?: string;
order?: number;
}
export interface ProjectImage {
src: string;
alt: string;
}
// ❌ BAD: Loose typing
type Project = {
slug: any;
title: any;
// Missing fields
};
Type Guards¶
Use type guards for runtime type checking:
// ✅ GOOD: Type guard function
function isProject(data: unknown): data is Project {
return (
typeof data === "object" &&
data !== null &&
"slug" in data &&
"title" in data &&
"description" in data &&
Array.isArray((data as Project).technologies)
);
}
// Usage
const data = JSON.parse(fileContent);
if (isProject(data)) {
// TypeScript knows data is Project here
console.log(data.title);
}
Utility Types¶
Leverage TypeScript utility types:
// Partial - make all properties optional
type PartialProject = Partial<Project>;
// Pick - select specific properties
type ProjectSummary = Pick<Project, "slug" | "title" | "shortDescription">;
// Omit - exclude properties
type ProjectWithoutImages = Omit<Project, "images">;
// Required - make all properties required
type RequiredProject = Required<Project>;
// Example usage
function updateProject(slug: string, updates: Partial<Project>) {
// updates can have any subset of Project properties
}
Generic Functions¶
Use generics for reusable functions:
// ✅ GOOD: Generic filter function
function filterByField<T, K extends keyof T>(items: T[], field: K, value: T[K]): T[] {
return items.filter((item) => item[field] === value);
}
// Usage
const reactProjects = filterByField(projects, "technologies", "React");
React Component Patterns¶
Server Components (Default)¶
Use server components by default:
// app/projects/page.tsx (Server Component)
import { getProjects } from '@/lib/projects';
import { ProjectCard } from '@/components/project-card';
export default async function ProjectsPage() {
// Data fetching in server component
const projects = await getProjects();
return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard key={project.slug} project={project} />
))}
</div>
);
}
Client Components¶
Use client components only when needed:
// components/theme-toggle.tsx
'use client';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
Toggle Theme
</Button>
);
}
When to use client components:
- Using React hooks (
useState,useEffect, etc.) - Event handlers (
onClick,onChange, etc.) - Browser APIs (
window,document, etc.) - Third-party libraries that require client-side JavaScript
Component Props¶
Define props with interfaces:
// ✅ GOOD: Explicit interface
interface ProjectCardProps {
project: Project;
className?: string;
variant?: 'default' | 'compact' | 'featured';
onSelect?: (project: Project) => void;
}
export function ProjectCard({
project,
className,
variant = 'default',
onSelect,
}: ProjectCardProps) {
return (
<Card className={cn('project-card', className)} onClick={() => onSelect?.(project)}>
{/* Component content */}
</Card>
);
}
// ❌ BAD: Inline props without interface
export function ProjectCard({ project, className }: { project: any; className?: string }) {
// ...
}
Component Composition¶
Favor composition over prop drilling:
// ✅ GOOD: Composition pattern
export function ProjectCard({ children }: { children: React.ReactNode }) {
return <Card>{children}</Card>;
}
export function ProjectCardHeader({ children }: { children: React.ReactNode }) {
return <CardHeader>{children}</CardHeader>;
}
export function ProjectCardContent({ children }: { children: React.ReactNode }) {
return <CardContent>{children}</CardContent>;
}
// Usage
<ProjectCard>
<ProjectCardHeader>
<h3>{project.title}</h3>
</ProjectCardHeader>
<ProjectCardContent>
<p>{project.description}</p>
</ProjectCardContent>
</ProjectCard>
// ❌ BAD: Prop drilling
<ProjectCard
title={project.title}
description={project.description}
headerClassName="..."
contentClassName="..."
showBadge={true}
// Too many props!
/>
Custom Hooks¶
Extract reusable logic into hooks:
// hooks/use-projects.ts
import { useState, useEffect } from 'react';
import type { Project } from '@/lib/projects';
export function useProjects() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch('/api/projects')
.then((res) => res.json())
.then((data) => {
setProjects(data);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, []);
return { projects, loading, error };
}
// Usage in component
'use client';
export function ProjectList() {
const { projects, loading, error } = useProjects();
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{projects.map((project) => (
<ProjectCard key={project.slug} project={project} />
))}
</div>
);
}
Data Management¶
Data Fetching¶
Use server-side data fetching:
// ✅ GOOD: Server-side data fetching
// app/projects/page.tsx
export default async function ProjectsPage() {
const projects = await getProjects(); // Runs on server
return <ProjectList projects={projects} />;
}
// ❌ BAD: Client-side data fetching for static data
'use client';
export default function ProjectsPage() {
const [projects, setProjects] = useState([]);
useEffect(() => {
fetch('/api/projects').then(/* ... */); // Unnecessary client-side fetch
}, []);
}
Caching Strategy¶
Implement appropriate caching:
// lib/projects.ts - Cache in memory for repeated calls
let projectsCache: Project[] | null = null;
let cacheTimestamp: number = 0;
const CACHE_TTL = 60000; // 1 minute
export async function getProjects(): Promise<Project[]> {
const now = Date.now();
if (projectsCache && now - cacheTimestamp < CACHE_TTL) {
return projectsCache;
}
const projects = await loadProjectsFromFiles();
projectsCache = projects;
cacheTimestamp = now;
return projects;
}
Error Handling¶
Handle errors gracefully:
// ✅ GOOD: Comprehensive error handling
export async function getProjectBySlug(slug: string): Promise<Project | null> {
try {
const projects = await getProjects();
const project = projects.find((p) => p.slug === slug);
if (!project) {
console.warn(`Project not found: ${slug}`);
return null;
}
return project;
} catch (error) {
console.error('Error fetching project:', error);
// Don't throw - return null for graceful degradation
return null;
}
}
// Usage in component
export default async function ProjectPage({ params }: { params: { slug: string } }) {
const project = await getProjectBySlug(params.slug);
if (!project) {
notFound(); // Next.js 404 page
}
return <ProjectDetail project={project} />;
}
Styling Patterns¶
Tailwind CSS¶
Use utility classes with mobile-first approach:
// ✅ GOOD: Mobile-first, responsive design
<div className="
flex flex-col gap-4 // Mobile: vertical stack
md:flex-row md:gap-6 // Tablet: horizontal layout
lg:gap-8 // Desktop: larger gaps
p-4 md:p-6 lg:p-8 // Responsive padding
">
{/* Content */}
</div>
// ❌ BAD: Desktop-first approach
<div className="
flex-row gap-8 // Assumes desktop
md:flex-col md:gap-4 // Has to override for mobile
">
Conditional Styling¶
Use cn() utility for conditional classes:
import { cn } from '@/lib/utils';
// ✅ GOOD: Using cn() for conditional classes
<Button
className={cn(
'rounded-lg px-4 py-2',
isActive && 'bg-primary text-primary-foreground',
isDisabled && 'opacity-50 cursor-not-allowed',
variant === 'outline' && 'border border-primary',
className // Allow override from props
)}
>
Click me
</Button>
// ❌ BAD: Manual string concatenation
<Button
className={
'rounded-lg px-4 py-2 ' +
(isActive ? 'bg-primary text-primary-foreground ' : '') +
(isDisabled ? 'opacity-50 cursor-not-allowed ' : '')
}
>
Component Variants¶
Use class-variance-authority for variant patterns:
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 px-3',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
Dark Mode¶
Use CSS variables for theme-aware styling:
/* globals.css */
:root {
--primary: oklch(0.48 0.0813 198.19);
--primary-foreground: #ffffff;
}
.dark {
--primary: oklch(0.68 0.13 264.2);
--primary-foreground: #f4f4f5;
}
// Components automatically adapt to theme
<div className="bg-primary text-primary-foreground">
{/* Works in both light and dark mode */}
</div>
Performance Optimization¶
Image Optimization¶
Use Next.js Image component:
import Image from 'next/image';
// ✅ GOOD: Optimized images
<Image
src="/images/projects/project-1/screenshot.jpg"
alt="Project screenshot"
width={800}
height={600}
priority // For above-the-fold images
placeholder="blur"
blurDataURL="data:image/..." // Or import image for automatic blur
/>
// ❌ BAD: Regular img tag
<img src="/images/projects/project-1/screenshot.jpg" alt="Screenshot" />
Code Splitting¶
Use dynamic imports for large components:
import dynamic from 'next/dynamic';
// ✅ GOOD: Lazy load heavy components
const ProjectGallery = dynamic(() => import('@/components/project-gallery'), {
loading: () => <div>Loading gallery...</div>,
ssr: false, // Disable SSR if not needed
});
export default function ProjectPage() {
return (
<div>
<h1>Project Title</h1>
<ProjectGallery />
</div>
);
}
Memoization¶
Memoize expensive computations:
'use client';
import { useMemo } from 'react';
export function ProjectList({ projects }: { projects: Project[] }) {
// ✅ GOOD: Memoize filtered results
const featuredProjects = useMemo(
() => projects.filter((p) => p.order && p.order <= 6),
[projects]
);
const sortedProjects = useMemo(
() => projects.sort((a, b) => a.title.localeCompare(b.title)),
[projects]
);
return (
<div>
<section>
<h2>Featured</h2>
{featuredProjects.map((p) => (
<ProjectCard key={p.slug} project={p} />
))}
</section>
<section>
<h2>All Projects</h2>
{sortedProjects.map((p) => (
<ProjectCard key={p.slug} project={p} />
))}
</section>
</div>
);
}
SEO Best Practices¶
Metadata Generation¶
Generate metadata for all pages:
// app/projects/[slug]/page.tsx
import { Metadata } from "next";
import { getProjectBySlug } from "@/lib/projects";
import { siteConfig } from "@/lib/config";
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const project = await getProjectBySlug(params.slug);
if (!project) {
return {
title: "Project Not Found",
};
}
return {
title: project.title,
description: project.shortDescription,
openGraph: {
title: project.title,
description: project.shortDescription,
type: "article",
url: `${siteConfig.url}/projects/${project.slug}`,
images: project.images.map((img) => ({
url: img.src,
alt: img.alt,
})),
},
twitter: {
card: "summary_large_image",
title: project.title,
description: project.shortDescription,
images: [project.images[0]?.src],
},
};
}
Structured Data¶
Add JSON-LD structured data:
// components/meta/structured-data.tsx
export function ProjectJsonLd({ project }: { project: Project }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CreativeWork',
name: project.title,
description: project.description,
author: {
'@type': 'Person',
name: siteConfig.author.name,
},
keywords: project.technologies.join(', '),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
Security Best Practices¶
Environment Variables¶
Never expose secrets in client code:
// ✅ GOOD: Server-only secret
// lib/server-only-function.ts
import { headers } from "next/headers";
export async function sendWebhook(data: unknown) {
const webhookUrl = process.env.WEBHOOK_URL; // Server-only
await fetch(webhookUrl, {
method: "POST",
body: JSON.stringify(data),
});
}
// ❌ BAD: Exposing secret in client
("use client");
export function MyComponent() {
const webhookUrl = process.env.WEBHOOK_URL; // undefined in client!
}
Input Validation¶
Validate all user input:
// ✅ GOOD: Input validation
export async function getProjectBySlug(slug: string): Promise<Project | null> {
// Validate slug format
if (!/^[a-z0-9-]+$/.test(slug)) {
console.warn("Invalid slug format:", slug);
return null;
}
const projects = await getProjects();
return projects.find((p) => p.slug === slug) || null;
}
Sanitize Content¶
Sanitize dynamic content:
// ✅ GOOD: Safe content rendering
export function ProjectDescription({ description }: { description: string }) {
// React automatically escapes text content
return <p>{description}</p>;
}
// ⚠️ CAUTION: Only if you trust the source
export function RichContent({ html }: { html: string }) {
// Use DOMPurify or similar library
return <div dangerouslySetInnerHTML={{ __html: sanitize(html) }} />;
}
Testing Patterns¶
Unit Tests¶
Test utility functions:
// lib/__tests__/utils.test.ts
import { cn, isVideoFile } from "@/lib/utils";
describe("cn", () => {
it("should merge class names", () => {
expect(cn("px-4", "py-2")).toBe("px-4 py-2");
});
it("should handle conditional classes", () => {
expect(cn("base", false && "hidden", true && "visible")).toBe("base visible");
});
});
describe("isVideoFile", () => {
it("should detect video extensions", () => {
expect(isVideoFile("video.mp4")).toBe(true);
expect(isVideoFile("video.webm")).toBe(true);
expect(isVideoFile("image.jpg")).toBe(false);
});
it("should be case insensitive", () => {
expect(isVideoFile("VIDEO.MP4")).toBe(true);
});
});
Component Tests¶
Test component rendering:
// components/__tests__/project-card.test.tsx
import { render, screen } from '@testing-library/react';
import { ProjectCard } from '@/components/project-card';
const mockProject: Project = {
slug: 'test-project',
title: 'Test Project',
shortDescription: 'A test project',
description: 'Full description',
technologies: ['React', 'TypeScript'],
images: [],
};
describe('ProjectCard', () => {
it('should render project title', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('Test Project')).toBeInTheDocument();
});
it('should render technologies', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('React')).toBeInTheDocument();
expect(screen.getByText('TypeScript')).toBeInTheDocument();
});
});
See Also¶
- Contributing Guide - Development workflow
- Troubleshooting - Common issues
- Tailwind Configuration - Styling setup
- Utility Functions - Helper functions
Next Steps¶
- Review the patterns above
- Apply them to new code
- Refactor existing code to follow patterns
- Share knowledge with team members
Last Updated: February 2026
Maintainers: Simon Stijnen
Questions? Open an issue on GitHub