From the sandbox to a project
In short: Vite turns the components into a real project: npm create vite makes the folders, npm run build puts together dist/ for publishing, and VITE_ environment variables are built into the site's code, so they are public.
Problem
Works in the sandbox, not in the browser
Your shop works in the sandbox. You copy the files into a folder, serve it and open the page. It is empty, and the console says:
Uncaught TypeError: Failed to resolve module specifier "vue". Relative references must start with either "/", "./", or "../".import { createApp } from 'vue' is not a path to a file, and the browser does not know where Vue is. And App.vue is not JavaScript at all: the browser does not understand <template> and <script setup>.
The sandbox in the lessons did this work for you. It translates the .vue files to JavaScript in the lesson page and knows where to get the library. In a real project a tool does it.
The idea
Vite: a development server and a build
Vite is the tool most new React and Vue projects are made with. An empty project takes one command:
npm create vite@latest shop -- --template vuecd shopnpm installnpm run dev--template picks the template: vue or vue-ts (with TypeScript). The two dashes -- before it tell npm that the flag is for create-vite, not for npm itself. npm install downloads the libraries into a node_modules folder.
Vite has two jobs, and package.json gives them as scripts:
npm run devstarts a development server athttp://localhost:5173. It translates each file when the browser asks for it. Save a file, and the page updates right away.npm run buildmakes the finished site in adist/folder. This is the build: everything translated and joined into plain files.npm run previewservesdist/athttp://localhost:4173, so you can see the build before you publish it.
The idea
The folders and the rules
The template gives you little. Here is the same shop once it grows:
shop/ index.html the entry: <script type="module" src="/src/main.js"> package.json the libraries and the scripts dev, build, preview vite.config.js the settings of Vite (the Vue plugin) public/ files copied as they are (favicon.svg) src/ main.js createApp(App).use(router).use(pinia).mount('#app') App.vue components/ ProductCard.vue, CartSummary.vue pages/ CatalogPage.vue, ProductPage.vue (for the router) stores/ cart.js: the Pinia store api/ products.js: the functions with fetch assets/ images the code brings in with importindex.html, public/ and src/main belong to Vite. The other folders are an agreement in the team, not a rule: Vite does not need them. The agreement helps you find things without searching. The tests from the previous lesson sit next to the component: ProductCard.test.<Only when="react">jsx</Only><Only when="vue">js</Only>.
Linting and formatting. A linter reads the code and catches mistakes before you run it: a hook inside an if, a forgotten variable. Formatting (for example with Prettier) arranges spaces and quotes the same way for the whole team, so nobody argues about them in a code review. The Vite template for Vue has no linter. The official way to start a Vue project, npm create vue@latest, asks what you want: TypeScript, Router, Pinia, Vitest, Playwright, ESLint or Oxlint, Prettier.
See it
What the build does
How a few files with .vue and import become a site the browser understands.
import { createApp } from 'vue';import App from './App.vue';import './style.css';
createApp(App).mount('#app');
// App.vueimport ProductCard from './components/ProductCard.vue';import hero from './assets/hero.png';
// npm run build → dist/// dist/index.html// dist/assets/index-[hash].js// dist/assets/index-[hash].css// dist/assets/hero-[hash].pngStep by step, in words
- The build starts at index.html. It loads main.js with <script type="module">, and main.js is where the app begins.
- Vite follows every import, file by file: App, ProductCard, the CSS, the image. A file nobody imports does not go in.
- All the JavaScript, the library from node_modules included, is joined into one file and shrunk: no spaces, short names.
- Out come plain files the browser understands: one .js, one .css and the image. Each name carries a hash computed from the content.
- dist/index.html now points to the new names. The whole dist/ folder is the site: you publish it as it is.
- You change ProductCard and build again. The JavaScript is different, so its name is new. The CSS did not change and keeps its name.
- So the browser may keep a hashed file forever: a new version always has a new name. Visitors download only what changed.
dist/ is the finished site: one HTML file and an assets/ folder. It needs no server, only a host that hands out files. It is not in git (the template puts it in .gitignore): you make it again for every release.
The idea
TypeScript in components
In the sandbox you can pass ProductCard anything: product={42}. The mistake shows only when it draws. TypeScript says what the component accepts and catches the mistake in the editor before you run anything:
<script setup lang="ts">import type { Product } from '../types';
const props = defineProps<{ product: Product }>();const emit = defineEmits<{ add: [product: Product] }>();</script>lang="ts" switches on TypeScript in the .vue file. defineProps<{ … }>() describes the props with a type instead of an array of names, and defineEmits describes what each event carries. If App passes price: '1.20', the check says Type 'string' is not assignable to type 'number'.
One thing surprises people: Vite only removes the types, it does not check them. That keeps it fast. The check is separate: in the TypeScript template npm run build is vue-tsc -b && vite build. If the types are wrong, the build stops before Vite starts.
Try it
Find the bug
The editor underlines one line in red. Which one?
Pick the line with the bug.
Line 12: product.prise. The type Product has no such field, and TypeScript says Property 'prise' does not exist on type 'Product'. Did you mean 'price'?. Without TypeScript product.prise is undefined, and the wrong price shows up only on the screen.
The idea
Environment variables are public
The address of the API is different on your computer and on the real site. So it is not written in the code, but in a .env file in the root of the project:
VITE_API_URL=https://api.shop.exampleconst API_URL = import.meta.env.VITE_API_URL;Vite gives the code only the variables that start with VITE_. On npm run build it writes the value into the JavaScript: dist/assets/index-….js contains https://api.shop.example as plain text. Anyone who opens the site can read it in the browser's tools.
So you never put a secret in a VITE_ variable: a payment key, a database password, an API key that costs money. It does not matter that .env is not in git. The secret is in the bundle. Secrets live on the server, and the page sends a request to your API (the lesson on secrets in "Security for developers").
Try it
Find the bug
The function pays for an order. Which line lets a secret out into the world?
Pick the line with the bug.
Line 2: the payment key is in a VITE_ variable, so it is in the JavaScript of the site. Anyone can take it and pay in your name. The page sends orderId to your API, and the server calls the payments with the key only it knows.
The idea
Publishing and the router's addresses
dist/ is only files, so any host for static sites will do. Publishing is: npm run build and copying dist/. Many hosts can do it for you on every push to git.
One detail from the routing lesson is left. The real site uses createWebHistory(), and the addresses look like /product/7. A customer opens such an address directly (from a link in an email or with a reload). The browser asks the host for /product/7, and dist/ has no such file: only index.html and assets/. The host says 404.
The fix is a rule on the host: every address that is not a file returns index.html. The app starts, and the router reads the address and draws ProductPage. This is called the SPA fallback. Every host writes it its own way, for example in nginx:
location / { try_files $uri $uri/ /index.html;}npm run dev and npm run preview do it by themselves, so the error shows only on the real host. Check it right after the first release: open a product address directly.
The idea
The next step: Next.js and Nuxt
Everything in this course is an app in the browser: the server gives an empty index.html and JavaScript, and the page is drawn on the customer's side. Until the JavaScript arrives and starts, the customer sees nothing.
Server-side rendering (SSR) draws the first HTML on the server. The page shows up sooner, and search engines read it without JavaScript. Then the same components wake up in the browser and become interactive. This is what the frameworks on top of the libraries do: Nuxt. They also add routing by files and code that runs only on the server (where secrets can live).
Components, props, state and effects are the same. So this course is the base, and the framework is the next lesson you will learn on your own.
More precisely, if you are curious
The development server does not bundle. With npm run dev the browser loads each module separately, and Vite translates it on request. That is why it starts in a moment even for a big project. Joining into one file happens only in the build.
Small images are inlined. An image under 4 KB brought in with import does not become a file: Vite puts it in the JavaScript as a data: address (the setting is build.assetsInlineLimit). Files from public/ are never touched and get no hash, so you refer to them by the full path: /favicon.svg.
The .env files. .env.local is only for your computer (the template keeps it out of git with *.local). .env.production applies to the build, and .env.development to npm run dev. import.meta.env.DEV and import.meta.env.PROD tell which mode the code is in. All values are text: VITE_PAGE_SIZE=20 gives '20', not 20.
A site in a subfolder. If the site lives at https://example.com/shop/, Vite has to know: base: '/shop/' in vite.config.js. Otherwise index.html points to /assets/… at the root and the files are not found.
npm run preview is not for the real site. It is for checking the build on your machine. The real site is served by a host or by a server such as nginx.
Exercise
From an empty folder to a build
Put the commands in order: a new project, the libraries, the development server, the build and the check of the build.
npm run buildnpm create vite@latest shop -- --template vuenpm run previewnpm run devnpm installcd shop
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Data from an API in a component · Shared state · Routing · Testing components
- Continues in: The shop front
- Tasks on this topic: A whole app