Vue projects are created with a scaffolder and built by Vite. Vite is by Evan You, the same author as Vue, and it replaced the old Vue CLI and its webpack build. In development it serves your source as native ES modules with no bundling at all, which is why the dev server starts in well under a second and stays fast as the project grows.
Creating a project
npm create vue@latest my-appIt asks a short series of yes/no questions — TypeScript, JSX, Vue Router, Pinia, testing, ESLint. Say yes to Router and Pinia to follow this track closely; everything else can be added later without pain.
cd my-app
npm install
npm run devThat serves the app on http://localhost:5173 and reloads as you edit.
What it generated
my-app/
index.html <- the real entry point, not a template
vite.config.js <- build and dev-server config
package.json
public/ <- copied to the output as-is, never processed
src/
main.js <- creates the app and mounts it
App.vue <- the root component
assets/ <- imported by your code, so it IS processed
components/
router/index.js <- if you chose Router
stores/ <- if you chose Piniaindex.html is the entry point
This is the part that surprises people arriving from webpack. In Vite, index.html is
not a template that a plugin injects tags into — it is the source file, and the
<script type="module"> in it is what pulls in your whole application.
<!doctype html>
<!-- data-bs-theme here rather than set from JS: doing it in script causes a
visible flash of the light theme before Vue mounts. -->
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>ReelCMS</title>
<meta name="description" content="Short-video content management, powered by MongoDB." />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>Two consequences worth knowing straight away. Anything you want in the document head — fonts, the favicon, meta tags — you write here, in ordinary HTML. And the build rewrites that script tag to point at the hashed bundle, so the file you author is the file that ships, with the URLs corrected.
<div id="app"></div> is the mount point. Everything Vue renders lives
inside it.
src/main.js creates the application
import { createApp } from "vue";
import { createPinia } from "pinia";
// The navbar toggle and any other data-bs-* behaviour need Bootstrap's JS. The
// bundle includes Popper, which the dropdown and tooltip plugins depend on.
import "bootstrap/dist/js/bootstrap.bundle.min.js";
import App from "./App.vue";
import router from "./router";
import "./assets/styles.css";
createApp(App).use(createPinia()).use(router).mount("#app");Read the last line right to left. createApp(App) builds an application instance
around the root component. Each .use(...) installs a plugin — Pinia
and the router are both plugins, which is why they are available to every component without any
component importing them. .mount("#app") renders it into that div.
Nothing is global. Two createApp calls on one page produce two independent
applications with their own plugins — which is what makes Vue practical for embedding a widget into
a page you do not own.
Note also that the CSS is imported, not linked. Vite treats a stylesheet as a module
like anything else, so it is processed, hashed and — in production — extracted into a real CSS
file.
vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
// 5176: pizza-react has 5173, and stayhub's two frontends take 5174 and
// 5175. strictPort so a collision fails loudly instead of silently moving
// the app to another port that nothing else is configured for.
port: 5176,
strictPort: true,
},
preview: { port: 4176, strictPort: true },
});plugins: [vue()] is the whole of what makes .vue files work. The rest
here is this application's own choice: a fixed port, and strictPort so a collision
fails loudly instead of silently moving the app to a port nothing else is configured for.
public/ versus src/assets/
A distinction that causes a lot of confused half-hours:
public/ is copied to the output untouched. Reference it by absolute
URL — /favicon.svg. Nothing hashes it and nothing checks it exists.
src/assets/ is imported by your code. Vite hashes it for cache
busting, inlines it if it is tiny, and fails the build if it is missing — which is
the reason to prefer it.
Talking to a backend in development
Your API almost certainly runs on a different port, and the browser will refuse the request. Two options.
The demo application points at the API's real origin and lets the server send CORS headers:
VITE_USE_MOCK=false
VITE_API_BASE=http://localhost:8087The alternative is a dev-server proxy, which sidesteps CORS entirely by making the request same-origin:
// vite.config.js -- inside defineConfig({ ... })
server: {
proxy: {
// Requests to /api are forwarded to the backend, so the browser only
// ever sees one origin and CORS never enters into it. Your fetch calls
// then use a relative "/api/reels" with no base URL at all.
"/api": { target: "http://localhost:8087", changeOrigin: true },
},
},Neither exists in production, where the built files are usually served by the same host as the API or by a CDN in front of it. Lesson 27 covers that.
Environment variables
Vite exposes variables on import.meta.env, and only those prefixed with
VITE_:
const BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8087";The prefix is a safety rail. Anything exposed this way is compiled into the bundle
and readable by anyone who opens the network tab, so the rule is that a secret never gets a
VITE_ prefix. Vite also reads .env at startup only —
changing it needs a dev-server restart, not a page reload.
The commands you will actually run
npm run dev # dev server, hot module replacement
npm run build # production build into dist/
npm run preview # serve dist/ locally, to check the real buildpreview is worth the habit. It serves the actual production output, which is where
you find the problems that only exist after the build — a missing environment variable, or routes
that 404 on refresh because the host has no history-mode rewrite.
Next: The Single-File Component — what is actually inside a
.vue file.