Building maintainable frontend applications requires more than just writing code—it demands thoughtful architecture, consistent patterns, and the right tools. This guide covers the practices we've refined through real-world projects.
A well-organized folder structure is the foundation of a scalable application. Here's what we recommend:
src
├── assets # Static files: images, fonts, icons
├── components # Shared components across the application
├── config # Global configuration and env variables
├── constants # Application constants
├── hoc # Higher-order components
├── hooks # Shared custom hooks
├── lib # Pre-configured library exports
├── mock # Mock data for static UI development
├── providers # Application providers (QueryClient, Auth, etc.)
├── api # REST API integration
│ ├── queries # GET requests
│ └── mutations # POST, PUT, PATCH, DELETE requests
├── routes # Route configuration
├── stores # Global state management (Zustand, etc.)
├── test # Test utilities and mock server
├── types # TypeScript base types
└── utils # Shared utility functions
This structure scales with your application. Each folder has a single responsibility, making it easy to locate code and onboard new team members.
Choose tools that complement each other and solve real problems. Here's our recommended stack:
| Use Case | Package | Notes | |----------|---------|-------| | Framework | Next.js, Vite | Next.js for full-stack; Vite for standalone React | | API Client | Axios + TanStack React Query | Query handles caching, synchronization, and state | | Forms | React Hook Form + Zod | Minimal re-renders, type-safe validation | | State Management | Zustand | Lightweight, TypeScript-first, minimal boilerplate | | UI Components | shadcn/ui | Unstyled, composable, Tailwind-ready | | Tables | TanStack React Table | Headless, powerful, zero dependencies | | Charts | Recharts | React-native, responsive, production-ready | | Notifications | Sonner | Toast notifications, minimal setup | | Carousel | Swiper | swiperjs.com/react | | Animation | Framer Motion | Declarative animations, great DX | | Dates | date-fns | Tree-shakeable, functional API | | Markdown Editor | react-quill | Rich text editing without complexity |
Pro tip: Avoid over-engineering. Start with TanStack React Query + Zustand for most apps. Add tools only when you have the problem they solve.
cn() Utility: Twin MergeTailwind's utility-first approach creates naming conflicts. Use tailwind-merge with clsx to intelligently resolve them:
import { type ClassValue, clsx } from 'clsx';
import tailwindConfig from '../../tailwind.config';
import { extendTailwindMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
const twMerge = extendTailwindMerge({
classGroups: {
'font-size': [{ text: Object.keys(tailwindConfig.theme?.extend?.fontSize || []) }],
},
});
return twMerge(clsx(inputs));
}
Use it to merge conditional styles without duplication:
<div className={cn('p-4 bg-white', isActive && 'bg-blue-500')} />
A flexible heading component that handles typography hierarchy and visual styling:
import * as React from 'react';
import { cn } from '@/utils/shade-cn';
type HeadingType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
interface BlockHeadingProps extends React.ComponentPropsWithRef<HeadingType> {
className?: string;
type: HeadingType;
hasLeftBorder?: boolean;
hasBottomBorder?: boolean;
}
const BlockHeading = React.forwardRef<HTMLHeadingElement, BlockHeadingProps>(
(
{
type: Tag,
hasLeftBorder = true,
hasBottomBorder = false,
className,
...props
},
ref,
) => {
return (
<Tag
ref={ref}
className={cn([
'relative text-content-heading',
Tag === 'h1' && 'text-h2 md:pl-8 md:text-h3 lg:text-h1',
Tag === 'h2' && 'text-h5 md:text-h4 lg:text-h2',
Tag === 'h3' && 'text-h5 md:text-h4 lg:text-h3',
Tag === 'h4' && 'text-h5 lg:text-h4',
Tag === 'h5' && 'text-h6 lg:text-h5',
hasLeftBorder &&
'pl-6 before:absolute before:left-0 before:top-0 before:h-full before:w-1 before:bg-accent-400',
hasBottomBorder && 'border-b border-[#E6E9E7] pb-5',
className,
])}
>
{props.children}
</Tag>
);
},
);
BlockHeading.displayName = 'BlockHeading';
export default BlockHeading;
Centralize layout constraints and responsive behavior:
import * as React from 'react';
import { cn } from '@/utils/shade-cn';
interface ContainerProps extends React.ComponentPropsWithRef<'div'> {
gridLayout?: boolean;
fluid?: boolean;
}
const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
({ className, gridLayout, fluid, children, ...props }, ref) => {
return (
<div
className={cn([
!fluid &&
'mx-auto w-full max-w-[calc(1320px+32px)] px-layout-margin-sm md:px-layout-margin-md lg:px-layout-margin-lg 2xl:max-w-[77rem] 3xl:max-w-[89.5rem] 4xl:max-w-[1832px]',
gridLayout && 'grid grid-cols-6 gap-x-5 md:grid-cols-12 lg:gap-x-4.5',
className,
])}
ref={ref}
{...props}
>
{children}
</div>
);
},
);
Container.displayName = 'Container';
export default Container;
Wrap your app with providers to enable caching, background sync, and data management:
'use client';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
interface ProvidersProps {
children?: React.ReactNode;
}
const queryClient = new QueryClient();
const Providers: React.FC<ProvidersProps> = (props) => {
return (
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
);
};
export default Providers;
Always use aspect ratio containers to prevent layout shift:
<div className='aspect-h-[619] aspect-w-[1350] relative mt-10 w-full rounded-lg'>
<Image
src={coverImage}
alt='Church Overview Image'
className='flex-shrink-0 rounded-lg object-cover'
fill
/>
</div>
A reusable divider component for visual separation:
import * as React from 'react';
import { cn } from '@/lib/cn';
interface SeparatorProps {
orientation?: 'horizontal' | 'vertical';
className?: string;
}
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
({ orientation, className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn([
'inline-block whitespace-pre shrink-0 bg-stroke',
orientation === 'horizontal' ? 'h-[0.063rem] w-full' : 'h-full w-[0.063rem]',
className,
])}
{...props}
/>
);
},
);
Separator.displayName = 'Separator';
export default Separator;
Usage:
<Separator orientation='horizontal' className='my-5' />
A comprehensive design system in Tailwind config enables consistency and scalability:
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['class'],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
extend: {
boxShadow: {
round: '0px 2px 2px 0px rgba(0, 0, 0, 0.10)',
popup: '1rem 1rem 2rem 0 rgba(0, 0, 0, 0.24)',
},
fontFamily: {
default: ['var(--font-inter)', 'sans-serif'],
heading: ['var(--font-figtree)', 'sans-serif'],
},
spacing: {
4.5: '1.125rem',
5.5: '1.375rem',
7.5: '1.875rem',
// ... additional spacing tokens
},
colors: {
content: {
heading: 'var(--text-heading)',
subtitle: 'var(--text-subtitle)',
body: 'var(--text-body)',
placeholder: 'var(--text-placeholder)',
disabled: 'var(--text-disabled)',
},
state: {
success: { base: 'var(--state-success-base)' },
error: { base: 'var(--state-error-base)' },
warning: { base: 'var(--state-warning-base)' },
},
// ... extensive color palette
},
fontSize: {
h1: ['2.625rem', { lineHeight: '1.2', letterSpacing: '-2', fontWeight: '700' }],
h2: ['2.188rem', { lineHeight: '1.2', fontWeight: '500', letterSpacing: '-2' }],
// ... typography scale
},
},
},
plugins: [require('tailwindcss-animate'), require('@tailwindcss/aspect-ratio')],
};
Define your design tokens at the root level:
@layer base {
:root {
/* Semantic colors */
--accent-purple: #9f1eba;
--accent-red: #d4145a;
--accent-green: #22b573;
/* State colors */
--state-success-base: #3cc9ae;
--state-error-base: #e64c4c;
--state-warning-base: #fdc854;
/* Text colors */
--text-heading: #010101;
--text-body: #010101bd;
--text-placeholder: #8a8a8a;
/* Stroke & spacing */
--stroke-default: #d9e2e8;
--black-80: #000000cc;
--white-100: #ffffff;
}
}
When building email templates, prioritize mobile-first responsive design:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
@media only screen and (max-width: 600px) {
table { width: 100% !important; }
td { padding: 10px !important; }
img { max-width: 100% !important; height: auto !important; }
}
</style>
</head>
<body style="margin: 0; padding: 0; font-family: 'Open Sans', sans-serif;">
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse; max-width: 700px;">
<!-- Email content here -->
</table>
</body>
</html>
Key principles:
Design & Assets:
Architecture & Best Practices:
| Issue | CSS Solution | Tailwind Class |
|-------|--------------|-----------------|
| Image stretching | object-fit: cover; | object-cover |
| Layout shift from images | Use aspect ratio containers | aspect-h-[619] aspect-w-[1350] |
| Text overflow | text-overflow: ellipsis; | truncate or line-clamp-3 |
When you need to reset to remote:
git reset --hard origin/dev
Build with intention. Your future self will thank you.