12 - Frontend Project Setup Guide (Interview Edition) ​
When you're given a blank screen in a coding round and told "set up a project from scratch," speed and confidence matter. This is your step-by-step playbook.
Quick Reference — Copy-Paste Commands ​
React + TypeScript (Vite) — RECOMMENDED ​
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run devReact + JavaScript (Vite) ​
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run devNext.js + TypeScript ​
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
cd my-app
npm run devFull Setup Walkthrough (Vite + React + TypeScript) ​
Step 1: Scaffold ​
npm create vite@latest my-app -- --template react-ts
cd my-app
npm installWhat you get out of the box:
my-app/
├── index.html # entry HTML
├── package.json
├── tsconfig.json # TS config (strict mode ON)
├── tsconfig.app.json # app-specific TS config
├── tsconfig.node.json # node-specific TS config (vite config)
├── vite.config.ts # vite config
├── eslint.config.js # ESLint flat config
├── src/
│ ├── main.tsx # entry point (renders <App />)
│ ├── App.tsx # root component
│ ├── App.css
│ ├── index.css
│ └── vite-env.d.ts # Vite type declarations
└── public/
└── vite.svgStep 2: Clean Up Boilerplate ​
Delete the default demo content so you start clean:
# Remove demo files
rm src/App.css src/assets/react.svg public/vite.svgReplace src/App.tsx with a clean starting point:
export default function App() {
return (
<main>
<h1>App</h1>
</main>
);
}Replace src/index.css with a minimal reset:
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
}Adding Common Tools ​
Tailwind CSS ​
npm install -D tailwindcss @tailwindcss/viteUpdate vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});Replace src/index.css:
@import "tailwindcss";Verify it works:
export default function App() {
return <h1 className="text-3xl font-bold text-blue-600">Working</h1>;
}React Router ​
npm install react-routerBasic setup in src/main.tsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router';
import App from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
</Routes>
</BrowserRouter>
</StrictMode>
);Testing (Vitest + React Testing Library) ​
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-eventAdd test config to vite.config.ts:
/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test-setup.ts',
},
});Create src/test-setup.ts:
import '@testing-library/jest-dom/vitest';Add script to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}Write your first test src/__tests__/App.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import App from '../App';
describe('App', () => {
it('renders heading', () => {
render(<App />);
expect(screen.getByRole('heading', { name: /app/i })).toBeInTheDocument();
});
});Run:
npm testPath Aliases (@ imports) ​
Update tsconfig.app.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}Update vite.config.ts:
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});Now you can do:
import { Button } from '@/components/Button';
import { useAuth } from '@/hooks/useAuth';Zustand (Lightweight State Management) ​
npm install zustandCreate a store src/store/useStore.ts:
import { create } from 'zustand';
interface AppState {
count: number;
increment: () => void;
reset: () => void;
}
export const useStore = create<AppState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}));TanStack Query (Server State) ​
npm install @tanstack/react-queryWrap app in src/main.tsx:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
retry: 1,
},
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);Usage:
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{data.map((u: User) => <li key={u.id}>{u.name}</li>)}</ul>;
}React Hook Form (Forms) ​
npm install react-hook-formimport { useForm } from 'react-hook-form';
interface FormData {
email: string;
amount: number;
}
function PaymentForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', { required: 'Email is required' })}
type="email"
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
<input
{...register('amount', { required: true, min: 1 })}
type="number"
placeholder="Amount"
/>
<button type="submit">Pay</button>
</form>
);
}Axios (HTTP Client) — Optional ​
npm install axios// src/services/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
headers: { 'Content-Type': 'application/json' },
});
// Request interceptor (attach auth token)
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Response interceptor (handle errors globally)
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// redirect to login
}
return Promise.reject(error);
}
);
export default api;Recommended Folder Structure ​
For a coding interview, keep it simple. Don't over-architect.
Small project (interview coding round) ​
src/
├── components/ # UI components
│ ├── Button.tsx
│ └── Card.tsx
├── hooks/ # Custom hooks
│ └── usePayment.ts
├── types/ # Shared TypeScript types
│ └── index.ts
├── utils/ # Pure utility functions
│ └── formatCurrency.ts
├── App.tsx
├── main.tsx
└── index.cssMedium project (take-home / real app) ​
src/
├── components/ # Reusable UI components
│ ├── ui/ # Base components (Button, Input, Card)
│ └── layout/ # Layout components (Header, Sidebar)
├── pages/ # Page-level components (one per route)
│ ├── Dashboard.tsx
│ └── Checkout.tsx
├── hooks/ # Custom hooks
├── context/ # React context providers
├── services/ # API client, external services
├── store/ # Zustand stores
├── types/ # TypeScript types/interfaces
├── utils/ # Pure helper functions
├── __tests__/ # Tests (or co-locate with components)
├── App.tsx
├── main.tsx
└── index.cssEnvironment Variables ​
Create .env at project root:
VITE_API_URL=http://localhost:3000/api
VITE_APP_NAME=MyAppAccess in code:
const apiUrl = import.meta.env.VITE_API_URL;Rules:
- Must be prefixed with
VITE_to be exposed to client code - Never put secrets in
VITE_variables (they're bundled into client JS) - Add
.envto.gitignore
Common Interview Scenarios — What to Install ​
"Build a todo app" ​
npm create vite@latest todo -- --template react-ts && cd todo && npm iNo extra deps needed. useState is enough.
"Build a form with validation" ​
npm create vite@latest form-app -- --template react-ts && cd form-app
npm i react-hook-form"Build a dashboard that fetches data" ​
npm create vite@latest dashboard -- --template react-ts && cd dashboard
npm i @tanstack/react-query axios
npm i -D tailwindcss @tailwindcss/vite"Build a multi-page app with routing" ​
npm create vite@latest app -- --template react-ts && cd app
npm i react-router"Build a checkout flow with state management and tests" ​
npm create vite@latest checkout -- --template react-ts && cd checkout
npm i zustand
npm i -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm i -D tailwindcss @tailwindcss/viteSpeed Tips for the Coding Round ​
Don't waste time on perfect setup. Scaffold → clean boilerplate → start coding. Total setup should be < 3 minutes.
Skip Tailwind if not asked. Inline styles or plain CSS are fine for a coding round. Don't spend 5 minutes configuring Tailwind if the task is about logic.
Skip routing if single page. Don't add React Router for a single-page task.
Start with types. Define your interfaces first — it structures your thinking and shows TypeScript fluency.
Build state layer before UI. Reducer/store first, then components that consume it. Easier to test and discuss.
Co-locate tests. Put test files next to the component (
Button.test.tsxnext toButton.tsx) — faster to navigate during the interview.Know the vite.config.ts test config by heart. The vitest setup (globals, jsdom, setupFiles) is always the same. Memorize it.
Use
npxif unsure about global installs.npx create-vite@latestworks even if vite isn't globally installed.