Version 1.1 – Last updated 2025-08-08
gist: lemyskaman/TS-JS-nodejs-react-vite-style-guide.md
A comprehensive coding style guide for full-stack Node.js applications with React frontend, TypeScript, and Vite compiler. This guide extends the base Node.js standards to include modern full-stack development patterns.
- Enhanced Project Structure (Full-Stack Monorepo)
- React + TypeScript Guidelines
- Vite Configuration & Optimization
- Shared Component Libraries
- State Management Patterns
- API Integration & Data Flow
- Full-Stack Development Tooling
- Testing Strategy (Full-Stack)
- Build & Deployment Pipeline
- Performance Optimization (Full-Stack)
Based on modern best practices for React + Node.js full-stack applications, here's the optimal project organization:
project-root/
├── apps/ # Applications
│ ├── web/ # React web app (Vite)
│ │ ├── public/
│ │ │ ├── index.html
│ │ │ └── favicon.ico
│ │ ├── src/
│ │ │ ├── app/ # App-level setup
│ │ │ │ ├── App.tsx
│ │ │ │ ├── main.tsx
│ │ │ │ └── router.tsx
│ │ │ ├── features/ # Feature-based organization
│ │ │ │ ├── auth/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── LoginForm/
│ │ │ │ │ │ │ ├── LoginForm.tsx
│ │ │ │ │ │ │ ├── LoginForm.test.tsx
│ │ │ │ │ │ │ └── LoginForm.module.css
│ │ │ │ │ │ └── SignupForm/
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── useAuth.ts
│ │ │ │ │ │ └── useLogin.ts
│ │ │ │ │ ├── services/
│ │ │ │ │ │ └── authApi.ts
│ │ │ │ │ ├── store/
│ │ │ │ │ │ └── authSlice.ts
│ │ │ │ │ ├── types/
│ │ │ │ │ │ └── auth.types.ts
│ │ │ │ │ └── pages/
│ │ │ │ │ ├── LoginPage.tsx
│ │ │ │ │ └── SignupPage.tsx
│ │ │ │ ├── user-management/
│ │ │ │ └── dashboard/
│ │ │ ├── shared/ # Shared frontend concerns
│ │ │ │ ├── components/ # Reusable UI components
│ │ │ │ │ ├── ui/ # Basic UI elements
│ │ │ │ │ │ ├── Button/
│ │ │ │ │ │ ├── Input/
│ │ │ │ │ │ ├── Modal/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── layout/
│ │ │ │ │ │ ├── Header/
│ │ │ │ │ │ ├── Sidebar/
│ │ │ │ │ │ └── Footer/
│ │ │ │ │ └── forms/
│ │ │ │ ├── hooks/ # Shared custom hooks
│ │ │ │ ├── services/ # API clients, utilities
│ │ │ │ ├── store/ # Global state management
│ │ │ │ ├── types/ # Shared TypeScript types
│ │ │ │ ├── utils/ # Helper functions
│ │ │ │ └── styles/ # Global styles & themes
│ │ │ ├── assets/ # Static assets
│ │ │ └── __tests__/ # Global test utilities
│ │ ├── vite.config.ts
│ │ ├── tsconfig.json
│ │ ├── package.json
│ │ └── .env.example
│ │
│ └── api/ # Node.js backend
│ ├── src/
│ │ ├── features/ # Vertical slice architecture
│ │ ├── shared/
│ │ ├── types/
│ │ ├── config/
│ │ └── app.js
│ └── ...
│
├── packages/ # Shared packages
│ ├── shared-types/ # Shared TypeScript definitions
│ ├── ui-components/ # Shared React component library
│ └── eslint-config/ # Shared ESLint configuration
│
├── tools/ # Development & build tools
│ ├── scripts/
│ └── generators/
│
├── docs/ # Documentation
├── .github/ # GitHub workflows
├── package.json # Root package.json (workspaces)
├── tsconfig.base.json # Base TypeScript config
├── docker-compose.yml # Development environment
└── README.md
- Monorepo Organization: Use npm/yarn workspaces for dependency management24
- Feature-First Structure: Both frontend and backend organized by business features56
- Shared Packages: Common types, components, and configurations78
- Tool Separation: Different tools for different concerns (Vite for frontend, Node.js for backend)910
// ✅ Good - Functional component with TypeScript
import React from 'react';
import styles from './UserCard.module.css';
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
interface UserCardProps {
user: User;
onEdit?: (userId: string) => void;
isEditable?: boolean;
className?: string;
}
export const UserCard: React.FC<UserCardProps> = ({
user,
onEdit,
isEditable = false,
className
}) => {
const handleEditClick = () => {
onEdit?.(user.id);
};
return (
<div className={`${styles.userCard} ${className || ''}`}>
<img
src={user.avatar || '/default-avatar.png'}
alt={`${user.name}'s avatar`}
className={styles.avatar}
/>
<div className={styles.info}>
<h3 className={styles.name}>{user.name}</h3>
<p className={styles.email}>{user.email}</p>
{isEditable && (
<button onClick={handleEditClick} className={styles.editButton}>
Edit
</button>
)}
</div>
</div>
);
};
export default UserCard;// ✅ Good - Custom hook with TypeScript
import { useState, useEffect, useCallback } from 'react';
interface UseApiOptions {
immediate?: boolean;
}
interface UseApiReturn<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
export const useApi = <T>(
apiCall: () => Promise<T>,
options: UseApiOptions = {}
): UseApiReturn<T> => {
const { immediate = true } = options;
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const result = await apiCall();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}, [apiCall]);
useEffect(() => {
if (immediate) {
fetchData();
}
}, [fetchData, immediate]);
return {
data,
loading,
error,
refetch: fetchData
};
};| Component Type | Location | Naming Convention | Example |
|---|---|---|---|
| Feature Components | features/[feature]/components/ |
PascalCase | UserProfile.tsx |
| Shared UI Components | shared/components/ui/ |
PascalCase | Button.tsx |
| Layout Components | shared/components/layout/ |
PascalCase | Header.tsx |
| Page Components | features/[feature]/pages/ |
PascalCase + "Page" | UserListPage.tsx |
| Hook Files | hooks/ or features/[feature]/hooks/ |
camelCase + "use" prefix | useAuth.ts |
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { loadEnv } from 'vite';
export default defineConfig(({ command, mode }) => {
// Load environment variables
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
react({
// Enable React Fast Refresh
fastRefresh: true,
// JSX runtime configuration
jsxRuntime: 'automatic'
})
],
// Path resolution
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@/components': resolve(__dirname, 'src/shared/components'),
'@/hooks': resolve(__dirname, 'src/shared/hooks'),
'@/utils': resolve(__dirname, 'src/shared/utils'),
'@/types': resolve(__dirname, 'src/shared/types'),
'@/assets': resolve(__dirname, 'src/assets')
}
},
// Development server configuration
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: env.VITE_API_URL || 'http://localhost:8000',
changeOrigin: true,
secure: false
}
}
},
// Build optimizations
build: {
outDir: 'dist',
sourcemap: mode !== 'production',
minify: 'terser',
terserOptions: {
compress: {
drop_console: mode === 'production'
}
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu']
}
}
},
chunkSizeWarningLimit: 1000
},
// Environment variables prefix
envPrefix: 'VITE_',
// CSS configuration
css: {
modules: {
localsConvention: 'camelCase'
},
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
},
// Testing configuration (Vitest)
test: {
environment: 'jsdom',
setupFiles: ['./src/__tests__/setup.ts'],
globals: true,
css: true
}
};
});// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
// Linting
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
// Path mapping (matches Vite aliases)
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/components/*": ["src/shared/components/*"],
"@/hooks/*": ["src/shared/hooks/*"],
"@/utils/*": ["src/shared/utils/*"],
"@/types/*": ["src/shared/types/*"],
"@/assets/*": ["src/assets/*"]
}
},
"include": ["src/**/*", "src/**/*.tsx", "vite-env.d.ts"],
"exclude": ["node_modules", "dist"]
}// packages/ui-components/src/components/Button/Button.tsx
import React from 'react';
import styles from './Button.module.css';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
icon?: React.ReactNode;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({
className,
variant = 'primary',
size = 'md',
loading,
icon,
children,
disabled,
...props
}, ref) => {
const classes = [
styles.button,
styles[variant],
styles[size],
className
].filter(Boolean).join(' ');
return (
<button
ref={ref}
className={classes}
disabled={disabled || loading}
{...props}
>
{loading && <span className={styles.spinner} />}
{icon && <span className={styles.icon}>{icon}</span>}
{children}
</button>
);
}
);
Button.displayName = 'Button';Storybook Configuration8
// packages/ui-components/.storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-design-tokens'
],
framework: {
name: '@storybook/react-vite',
options: {}
},
viteFinal: (config) => {
return config;
}
};
export default config;// apps/web/src/shared/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { authSlice } from '@/features/auth/store/authSlice';
import { userSlice } from '@/features/user-management/store/userSlice';
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
users: userSlice.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST']
}
}),
devTools: process.env.NODE_ENV !== 'production'
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;// apps/web/src/shared/hooks/useQueryClient.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userData: CreateUserRequest) =>
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
}).then(res => res.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
};// apps/web/src/shared/services/httpClient.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
class HttpClient {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor - Add auth token
this.instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - Handle errors
this.instance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.get(url, config);
return response.data;
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.post(url, data, config);
return response.data;
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.put(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.delete(url, config);
return response.data;
}
}
export const httpClient = new HttpClient();{
"name": "fullstack-app",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "concurrently \"npm run dev:api\" \"npm run dev:web\"",
"dev:api": "npm run dev --workspace=apps/api",
"dev:web": "npm run dev --workspace=apps/web",
"build": "npm run build --workspaces",
"test": "npm run test --workspaces",
"test:e2e": "playwright test",
"lint": "npm run lint --workspaces",
"lint:fix": "npm run lint:fix --workspaces",
"type-check": "npm run type-check --workspaces",
"storybook": "npm run storybook --workspace=packages/ui-components",
"clean": "npm run clean --workspaces && rm -rf node_modules"
},
"devDependencies": {
"concurrently": "^8.2.2",
"@playwright/test": "^1.40.0",
"husky": "^8.0.3",
"lint-staged": "^15.2.0"
}
}Frontend Testing Setup13
// apps/web/src/__tests__/setup.ts
import '@testing-library/jest-dom';
import { expect, afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock fetch
global.fetch = vi.fn();
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
vi.stubGlobal('localStorage', localStorageMock);// apps/web/src/shared/components/ui/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button loading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('applies correct variant classes', () => {
render(<Button variant="secondary">Secondary</Button>);
expect(screen.getByRole('button')).toHaveClass('secondary');
});
});# apps/web/Dockerfile
FROM node:18-alpine as build
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY apps/web/package*.json ./apps/web/
COPY packages/*/package*.json ./packages/
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY apps/web ./apps/web
COPY packages ./packages
# Build the application
WORKDIR /app/apps/web
RUN npm run build
# Production image
FROM nginx:alpine
# Copy built assets
COPY --from=build /app/apps/web/dist /usr/share/nginx/html
# Copy nginx configuration
COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]// Lazy loading components
import { lazy, Suspense } from 'react';
const UserManagementPage = lazy(() =>
import('../features/user-management/pages/UserManagementPage')
);
const AppRoutes = () => (
<Routes>
<Route
path="/users"
element={
<Suspense fallback={<div>Loading...</div>}>
<UserManagementPage />
</Suspense>
}
/>
</Routes>
);
// Memoization for expensive components
import { memo, useMemo } from 'react';
interface UserListProps {
users: User[];
searchTerm: string;
}
export const UserList = memo<UserListProps>(({ users, searchTerm }) => {
const filteredUsers = useMemo(() =>
users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
),
[users, searchTerm]
);
return (
<div>
{filteredUsers.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
});// vite.config.ts - Bundle analysis
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
// ... other plugins
visualizer({
filename: 'dist/stats.html',
open: true
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunks
'react-vendor': ['react', 'react-dom'],
'router-vendor': ['react-router-dom'],
// Feature-based chunks
'auth-feature': ['./src/features/auth'],
'user-feature': ['./src/features/user-management']
}
}
}
}
});This enhanced full-stack coding style guide provides a comprehensive foundation for building modern, scalable Node.js applications with React frontends. The combination of:
- Vertical Slice Architecture on the backend
- Feature-based organization on the frontend65
- Monorepo structure for code sharing342
- TypeScript throughout the stack1211
- Vite for optimal development experience141516
Creates a robust development environment that scales with team size and application complexity.
- Consistency Across Stack: Use TypeScript and consistent patterns in both frontend and backend
- Feature-First Organization: Structure both React and Node.js code around business features
- Shared Code: Leverage monorepo benefits for shared types, components, and utilities78
- Developer Experience: Vite + TypeScript + Hot reload = fast development cycles
- Production Ready: Include testing, linting, building, and deployment considerations from day one
Remember to adapt these guidelines to your specific project needs while maintaining the core principles of maintainability, scalability, and developer productivity.
Footnotes
-
https://dev.to/shubhadip_bhowmik/best-folder-structure-for-react-complex-projects-432p ↩
-
https://www.dhiwise.com/post/best-practices-for-structuring-your-react-monorepo ↩ ↩2 ↩3
-
https://dev.to/hardikidea/master-full-stack-monorepos-a-step-by-step-guide-2196 ↩ ↩2 ↩3
-
https://www.thatsoftwaredude.com/content/14110/creating-a-good-folder-structure-for-your-vite-app ↩ ↩2
-
https://dev.to/ricardo_maia_eb9c7a906560/sharing-components-micro-frontends-2p61 ↩ ↩2 ↩3
-
https://dzone.com/articles/component-library-with-lerna-monorepo-vite-and-sto ↩ ↩2 ↩3 ↩4
-
https://www.linkedin.com/pulse/best-practices-combining-reactjs-nodejs-modern-web-applications-4yswf ↩ ↩2
-
https://dev.to/shanu001x/how-to-setup-full-stack-project-for-production-in-nodejs-environment-2d7l ↩ ↩2
-
https://dev.to/janoskocs/setting-up-a-react-project-using-vite-typescript-vitest-2gl2 ↩
-
https://javascript.plainenglish.io/setting-up-a-react-typescript-project-with-vite-eslint-prettier-and-husky-ef7c9dada761 ↩
-
https://jurnal.itscience.org/index.php/brilliance/article/view/5971 ↩