| | |

Vite 03 ⚡ Project Structure — index.html as entry & src conventions

⏱️ Estimated read: 11 min | |

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

PathPurposeProcessed by Vite
index.html (root)The entry pointYes (parsed)
src/The application codeYes
src/assets/The imported assetsYes (hashed)
public/The static assetsNo (copied as-is)
vite.config.tsThe configurationN/A

The Entry Chain

FilePurpose
index.htmlThe HTML shell and the root element
src/main.tsxThe application entry, mounts the App
src/App.tsxThe root component
src/index.cssThe global stylesheet

The Public vs Assets

Use CaseLocation
The faviconpublic/
The robots.txtpublic/
The fonts referenced from the CSSpublic/
The images imported in the componentssrc/assets/
The logos used in the JSXsrc/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

PitfallWhy It HappensFix
The entry is not foundThe index.html is in the wrong placeMove it to the root
The public asset is not servedThe path is wrongUse the /asset.svg form
The asset is not hashedImported from public/Import from src/assets/
The alias does not resolveThe tsconfig.json paths are missingAdd 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

ItemValue
Entry pointindex.html in the project root
Entry script<script type="module" src="/src/main.tsx">
Public directorypublic/, served as-is, not processed
Source directorysrc/, processed and bundled
Source assetssrc/assets/, imported and hashed
Global stylessrc/index.css, imported in the entry
Alias@/* mapped to src/*
Config filevite.config.ts (optional)

Key takeaways:

  • The index.html is the entry point, and it is in the project root. It is not in the public/ directory and not in the src/ 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, the robots.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.tsx is the application entry. It imports the App, imports the global styles, and mounts the application into the #root element .
  • The @/* alias is the common convention. It replaces the deep relative paths, and it is configured in the vite.config.ts and the tsconfig.json .
  • The vite.config.ts is 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.


Was this helpful?


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!