Skip to content
wannacode

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:

Terminal
npm create vite@latest shop -- --template vue
cd shop
npm install
npm 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 dev starts a development server at http://localhost:5173. It translates each file when the browser asks for it. Save a file, and the page updates right away.
  • npm run build makes the finished site in a dist/ folder. This is the build: everything translated and joined into plain files.
  • npm run preview serves dist/ at http://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 import

index.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.vue
import 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].png
Step by step, in words
  1. The build starts at index.html. It loads main.js with <script type="module">, and main.js is where the app begins.
  2. Vite follows every import, file by file: App, ProductCard, the CSS, the image. A file nobody imports does not go in.
  3. All the JavaScript, the library from node_modules included, is joined into one file and shrunk: no spaces, short names.
  4. Out come plain files the browser understands: one .js, one .css and the image. Each name carries a hash computed from the content.
  5. dist/index.html now points to the new names. The whole dist/ folder is the site: you publish it as it is.
  6. You change ProductCard and build again. The JavaScript is different, so its name is new. The CSS did not change and keeps its name.
  7. 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.

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:

Terminal
VITE_API_URL=https://api.shop.example
const 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.

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.

  1. npm run build
  2. npm create vite@latest shop -- --template vue
  3. npm run preview
  4. npm run dev
  5. npm install
  6. cd shop

Test

Check yourself

1. What does npm run build do in a Vite project?
2. Why do the names in dist/assets/ have a hash (index-DSXtc1GT.js)?
3. The payment key is in VITE_PAYMENT_KEY in .env, and .env is not in git. Is it a secret?
4. The site is published. A customer opens /product/7 directly and gets a 404 from the host. What is missing?
5. The project uses TypeScript. vite build succeeds, and npm run build fails with a type error. Why?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·