Vite 03 ⚡ Project Structure — index.html as entry & src conventions
Vite’s project structure differs from the bundler-based tools in one defining way: the index.html is the entry point, and it lives in the project root, not in the public/ directory and not in the src/ directory . The previous chapter covered the scaffolding; this chapter explains what the generated files do, why the index.html is special, and how the src/ conventions organize the application code.
The index.html is not a static template that the bundler copies. It is the entry module that Vite parses to discover the JavaScript entry point. The <script type="module" src="/src/main.tsx"> tag inside the HTML is the first request the browser makes, and Vite follows that import to build the module graph on demand . The public/ directory is for the assets that Vite does not process, and the src/ directory is for the application code that Vite transforms and bundles. The distinction between the two is the first structural decision the developer makes.
Key point: The index.html must be in the project root. It is the entry point, and it references the /src/main.tsx (or the equivalent) through a <script type="module"> tag. The public/ directory is for the static assets that are served as-is, and the src/ directory is for the code that Vite processes and bundles.
Why the index.html is the entry point
The bundler-entry problem.
A traditional bundler starts from a JavaScript file and generates the HTML as a side effect. Vite starts from the HTML and discovers the JavaScript through the <script> tag. The index.html is the source of truth for the page structure: it contains the <div id="root"> where the application mounts, the <title> tag, and the <script> tag that imports the entry module . The HTML is the entry, and the JavaScript is the dependency.
The public-directory problem.
The public/ directory contains the files that Vite does not process. They are served at the root URL, and they are copied to the dist/ directory as-is during the build . A favicon, a robots.txt, or a font file belongs in public/ because it does not need the bundler’s transformation. The index.html does not belong in public/, because Vite must parse it to discover the module graph.
The src-directory problem.
The src/ directory contains the application code that Vite processes. The main.tsx (or main.jsx) is the entry module that mounts the application, and the App.tsx is the root component . The assets/ folder inside src/ is for the images and the fonts that are imported into the components, which allows Vite to hash the filenames and tree-shake the unused assets .
a. The root files and the entry chain
The project root contains the index.html, the package.json, the vite.config.ts, and the src/ and public/ directories.
my-app/
├── index.html # The entry point (in the root)
├── src/
│ ├── main.tsx # The application entry
│ ├── App.tsx # The root component
│ └── index.css # The global styles
├── public/ # The static assets (served as-is)
├── vite.config.ts # The Vite configuration (optional)
└── package.json
The index.html contains the root element and the module script.
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
The link rel="icon" href="/vite.svg" points to the public/vite.svg file, which is served at the root URL . The script type="module" src="/src/main.tsx" is the entry module, and Vite transforms the main.tsx and follows its imports .
The main.tsx imports the App and mounts it.
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)
The document.getElementById('root') finds the root element from the index.html, and the createRoot mounts the App into it . The import './index.css' is the global stylesheet, and Vite injects it into the page during the development and extracts it into a separate file during the build .
b. The public directory and the src assets
The public/ directory and the src/assets/ directory serve different purposes, and the choice between them depends on whether the asset needs the bundler’s processing.
public/
├── vite.svg # Served at /vite.svg
└── fonts/
└── custom.woff2 # Served at /fonts/custom.woff2
src/
└── assets/
└── logo.png # Imported and hashed
The public/vite.svg is referenced as /vite.svg in the HTML, and it is copied to the dist/ directory without the transformation . The src/assets/logo.png is imported in a component, and Vite processes it: it hashes the filename for the cache busting and includes it in the module graph .
// src/App.tsx
import logo from './assets/logo.png'
function App() {
return <img src={logo} alt="Logo" />
}
The import logo from './assets/logo.png' returns the URL of the processed asset, and the img src={logo} uses that URL. The asset is hashed in the production build, and the browser can cache it indefinitely because the filename changes when the content changes .
The rule is simple: use public/ for the files that need a fixed URL (the favicon, the robots.txt, the fonts referenced from the CSS), and use src/assets/ for the files that are imported into the components .
c. The src conventions and the aliases
The src/ directory has no enforced structure beyond the main.tsx entry. The common conventions are the components/, the pages/, the hooks/, the utils/, and the assets/ folders, but Vite does not require them .
src/
├── components/ # The reusable UI components
├── pages/ # The route-level components
├── hooks/ # The custom hooks
├── utils/ # The shared utilities
├── assets/ # The imported assets
├── App.tsx # The root component
└── main.tsx # The entry module
The aliases are configured in the vite.config.ts and the tsconfig.json. The @/* alias is the common convention for the src/ directory.
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
The resolve.alias maps the @ prefix to the src/ directory, which allows the imports like import { Button } from '@/components/Button' instead of the relative paths . The tsconfig.json must have the matching paths configuration for the TypeScript to resolve the same alias.
Complete Example Session
# ============================================
# PART 1: THE ROOT INDEX.HTML
# ============================================
# The index.html is in the project root, not in src/ or public/
ls index.html
# ============================================
# PART 2: THE ENTRY SCRIPT
# ============================================
# The index.html references the src/main.tsx
grep "src/main" index.html
# ============================================
# PART 3: THE PUBLIC DIRECTORY
# ============================================
ls public/
# The static assets served at the root
# ============================================
# PART 4: THE SRC ASSETS
# ============================================
ls src/assets/
# The imported assets processed by Vite
# ============================================
# PART 5: THE ALIAS CONFIG
# ============================================
# vite.config.ts and tsconfig.json both define the @ alias
# ============================================
# PART 6: THE BUILD OUTPUT
# ============================================
npm run build
ls dist/
# The index.html and the hashed assets
The six parts demonstrate the structure: the root index.html, the entry script, the public/ directory, the src/assets/ directory, the alias configuration, and the build output.
Quick Reference
Directory Roles
| Path | Purpose | Processed by Vite |
|---|---|---|
index.html (root) | The entry point | Yes (parsed) |
src/ | The application code | Yes |
src/assets/ | The imported assets | Yes (hashed) |
public/ | The static assets | No (copied as-is) |
vite.config.ts | The configuration | N/A |
The Entry Chain
| File | Purpose |
|---|---|
index.html | The HTML shell and the root element |
src/main.tsx | The application entry, mounts the App |
src/App.tsx | The root component |
src/index.css | The global stylesheet |
The Public vs Assets
| Use Case | Location |
|---|---|
| The favicon | public/ |
The robots.txt | public/ |
| The fonts referenced from the CSS | public/ |
| The images imported in the components | src/assets/ |
| The logos used in the JSX | src/assets/ |
Best Practices
✅ Do This:
<!-- Put the index.html in the root -->
<script type="module" src="/src/main.tsx"></script> # ✅
// Import the assets from the src/assets/
import logo from './assets/logo.png' # ✅
// Configure the @ alias in the vite.config.ts
resolve: { alias: { '@': path.resolve(__dirname, './src') } } # ✅
❌ Don’t Do This:
<!-- Put the index.html in the public/ -->
public/index.html // Vite does not process it # ❌
// Reference the public asset with the import
import logo from '../public/logo.png' // Wrong # ❌
// Use the relative paths for the deep imports
import { Button } from '../../../components/Button' // Use @ # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The entry is not found | The index.html is in the wrong place | Move it to the root |
| The public asset is not served | The path is wrong | Use the /asset.svg form |
| The asset is not hashed | Imported from public/ | Import from src/assets/ |
| The alias does not resolve | The tsconfig.json paths are missing | Add the matching paths |
Real-World Examples
1. The root index.html
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
2. The main.tsx entry
createRoot(document.getElementById('root')!).render(<App />)
3. The public favicon
<link rel="icon" href="/vite.svg" />
4. The imported asset
import logo from './assets/logo.png'
5. The alias configuration
resolve: { alias: { '@': path.resolve(__dirname, './src') } }
6. The tsconfig paths
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
7. The public directory
public/
├── vite.svg
└── robots.txt
8. The src assets
src/assets/
├── logo.png
└── hero.jpg
9. The build output
dist/
├── index.html
└── assets/
└── logo-abc123.png
10. The src structure
src/
├── components/
├── pages/
├── App.tsx
└── main.tsx
Visual
The Entry Chain
┌──────────────────────────────────────────────────────────────┐
│ THE INDEX.HTML IS THE ENTRY │
│ │
│ index.html (root) │
│ ├── <div id="root"> │
│ ├── <script src="/src/main.tsx"> │
│ └── <link href="/vite.svg"> │
│ │ │
│ ▼ │
│ src/main.tsx │
│ ├── imports App.tsx │
│ ├── imports index.css │
│ └── mounts the App into #root │
│ │ │
│ ▼ │
│ src/App.tsx │
│ └── the root component │
└──────────────────────────────────────────────────────────────┘
The Public vs the Assets
┌──────────────────────────────────────────────────────────────┐
│ PUBLIC IS SERVED AS-IS; ASSETS ARE PROCESSED │
│ │
│ public/vite.svg │
│ ├── Served at /vite.svg │
│ ├── Not processed by Vite │
│ └── Copied to dist/ as-is │
│ │
│ src/assets/logo.png │
│ ├── Imported in the component │
│ ├── Processed by Vite │
│ └── Hashed in the build: logo-abc123.png │
└──────────────────────────────────────────────────────────────┘
The Alias
┌──────────────────────────────────────────────────────────────┐
│ THE @ ALIAS REPLACES THE DEEP RELATIVE PATHS │
│ │
│ Without the alias: │
│ └── import { Button } from '../../../components/Button' │
│ │
│ With the alias: │
│ └── import { Button } from '@/components/Button' │
│ │
│ The alias is configured in the vite.config.ts and the │
│ tsconfig.json (the paths option). │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Entry point | index.html in the project root |
| Entry script | <script type="module" src="/src/main.tsx"> |
| Public directory | public/, served as-is, not processed |
| Source directory | src/, processed and bundled |
| Source assets | src/assets/, imported and hashed |
| Global styles | src/index.css, imported in the entry |
| Alias | @/* mapped to src/* |
| Config file | vite.config.ts (optional) |
Key takeaways:
- The
index.htmlis the entry point, and it is in the project root. It is not in thepublic/directory and not in thesrc/directory. Vite parses the HTML to discover the module graph . - The
public/directory is for the static assets that Vite does not process. The favicon, therobots.txt, and the fonts belong there, and they are served at the root URL . - The
src/assets/directory is for the assets that Vite processes. The images and the fonts imported into the components are hashed and included in the module graph . - The
src/main.tsxis the application entry. It imports theApp, imports the global styles, and mounts the application into the#rootelement . - The
@/*alias is the common convention. It replaces the deep relative paths, and it is configured in thevite.config.tsand thetsconfig.json. - The
vite.config.tsis optional. The project works without it, and the file is created when the custom configuration (the plugins, the aliases, the proxy) is needed .
Remember: Vite’s structure starts with the index.html in the root. The HTML is the entry, and the <script> tag points to the src/main.tsx. The public/ is for the fixed-URL assets, and the src/assets/ is for the imported and hashed assets. The @ alias replaces the deep relative paths. The result is the structure where the HTML is the source of truth and the JavaScript is the dependency, which is the opposite of the traditional bundler model.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!