Skip to content
beginner Phase 1 · Project Setup and Your First App

Scaffolding with create-vue

Generate a project with the official CLI and start the Vite dev server.

30m
0 problems
Topic Progress 0%

Answering the create-vue Prompt Tree Without Regret

You run npm create vue@latest, type a project name, and hit a wall of Yes/No questions: TypeScript, JSX, Vue Router, Pinia, Vitest, an end-to-end runner, ESLint, Prettier. Each answer changes which files land on disk, and a careless answer means editing package.json, vite.config.ts, and tsconfig.json by hand later — or regenerating the project and copying your work across. Ten seconds of thought now saves an afternoon of untangling.

create-vue is deliberately not a full-featured CLI. It is the official scaffolder maintained by the Vue team, and its only job is composing a Vite project out of officially supported pieces. That constraint is the point: every option it offers maps to a first-party or community-standard tool — Vue Router, Pinia, Vitest, eslint-plugin-vue, Prettier — and nothing it generates needs to be unwound later. When you outgrow a choice, you delete a config file. When you bolt on a tool the scaffolder does not know, you write the wiring yourself.

The prompt tree, and what each answer actually installs:

Prompt What it adds Skip it when
Add TypeScript? typescript, vue-tsc, tsconfig files, env.d.ts, .ts sources You need a prototype running today and the team does not write TS
Add JSX Support? @vitejs/plugin-vue-jsx and .tsx components You never render a component from a function
Add Vue Router? vue-router, src/router/index.ts, a views/ folder, wiring in App.vue Single-screen app or a component library
Add Pinia? pinia, src/stores/ scaffold State lives comfortably inside one component's refs
Add Vitest? vitest, @vue/test-utils, test files, a test:unit script You will not run unit tests in CI yet
Add ESLint / Prettier? eslint + eslint-plugin-vue (+ prettier), a lint script Never — take both

TypeScript has the largest blast radius, so decide it first. vue-tsc type-checks templates as well as script, which means a misspelled prop inside {{ }} fails the build instead of silently rendering blank. Adding TS after the fact means converting every file, repairing tsconfig references, and rewriting the build script — possible, but that is a migration, not a toggle.

Router and Pinia deserve the same honest question: do you have more than one route, and does state outlive a component? If both answers are no, skip them. The scaffold contains none of your code yet, so regenerating a project costs nothing except the answers you retype — which is exactly why you should answer for the app you are building this month, not the imaginary one you might build next year.

Notice what the CLI never asks: bundler, dev server, directory layout. Those debates are already settled. Vite builds and serves, src/ holds code, public/ holds static files copied verbatim, and index.html sits at the project root as the entry the dev server reads. The scaffolder removes the arguments that used to burn entire setup threads.

When the prompts finish, three commands get you running:

npm create vue@latest     # answer the prompt tree
cd my-app
npm install               # resolve dependencies against the generated lockfile
npm run dev               # Vite dev server on http://localhost:5173

In Application Entry and Mounting you will see what main.ts does with the files you just generated.

Why npm run dev Answers in Under a Second: Vite's Native-ESM Server

A teammate on a webpack-based app waits 40 seconds for their dev server and another 10 whenever they switch branches. You start a fresh create-vue project and the banner prints before you finish reading it. Same machine, same node_modules — the difference is what the dev server does before it can serve a single page.

The old model bundles first: walk the import graph from the entry, transform every module, concatenate them, then hand the browser one big file. Work you do before first paint scales with application size, so the larger the app, the longer the blank tab. Vite inverts the order. It serves index.html immediately, and the browser's own module loader pulls source files one at a time over HTTP, each transformed the instant it is requested.

That works because browsers already speak ES modules natively. Vite's dev server reads a file like src/components/UserCard.vue, compiles it (the @vitejs/plugin-vue plugin handles single-file components), rewrites imports to URLs the browser can fetch, and returns JavaScript. Nothing unrelated to the page currently on screen is ever parsed. Startup time is therefore nearly constant with respect to app size: a 5-component demo and a 500-component admin panel both answer on the first request.

Two things still happen eagerly, and knowing them explains the log output:

  1. Dependency pre-bundling. Many packages in node_modules ship CommonJS or UMD, which browsers cannot import, and some (like lodash-es) would otherwise fire hundreds of small requests. Vite converts them to ESM and flattens them into single files under node_modules/.vite/deps/ before serving. esbuild — a bundler written in Go — does this in well under a second where a JavaScript bundler would take minutes. The cache is keyed off your lockfile, so a package.json change re-triggers it and you see optimized dependencies changed. reloading in the terminal.
  2. On-demand transforms. Every other file is compiled lazily. The first click that navigates to a heavy route pays a small request cost; the results are cached until you edit the file.

Hot module replacement (HMR) is the part you feel day to day. When you save a file, Vite does not reload the page: it sends the new module over a WebSocket, and the module's importer chain runs an accept callback that swaps the code in place. The Vue plugin compiles every SFC to a module that accepts its own updates, so editing a <script setup> block replaces the component definition while the live instance — and its refs — survive. That is why a running counter does not reset when you fix a typo. When the changed file sits above any accept boundary (say you edit main.ts), HMR cannot stitch it in and Vite falls back to a full reload; the console shows [vite] page reload.

Useful flags on the same script:

npm run dev -- --host        # expose on the LAN so a phone can hit your machine
npm run dev -- --port 3000   # Vite otherwise picks 5173 and increments if busy
npx vite --strictPort        # fail loudly instead of silently moving to 5174

If the server prints Port 5173 is in use, trying another one..., your URL changed and any bookmark or WebSocket client pointed at 5173 is now stale — strictPort turns that confusion into an error you can act on.

The production build behaves differently on purpose: npm run build hands the same graph to Rollup, emits hashed assets into dist/, and only then optimizes for total bytes instead of time-to-first-module.

Reading the Generated package.json: dev, build, preview, type-check

The most honest description of a scaffold is its package.json. Everything the prompt tree decided shows up as a dependency, and everything you can do to the project shows up as a script. Open yours and read it line by line — a typical create-vue project with TypeScript, Vitest, ESLint and Prettier selected looks like this:

{
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --",
    "build-only": "vite build",
    "preview": "vite preview",
    "test:unit": "vitest",
    "type-check": "vue-tsc --build",
    "lint": "eslint . --fix"
  }
}

Read what each command actually does, because the names hide distinctions that matter in review and CI.

npm run dev starts the Vite dev server described above: on-demand transforms, HMR, no minification, no output directory. It never writes files; kill it and nothing on disk changed.

npm run build is the gate. In a plain JavaScript project it is just vite build, which runs Rollup, tree-shakes unused exports, splits chunks by dynamic import(), hashes filenames for cache busting, and writes everything to dist/. The TypeScript project wraps it: run-p type-check "build-only {@}" -- launches vue-tsc --build and vite build in parallel, and the build fails if either fails. This is the detail interviewers like — a vite build alone never type-checks your code, because esbuild strips types without inspecting them. If you bypass the wrapper script, a confidently wrong props access compiles fine and breaks at runtime.

npm run preview serves the contents of dist/ as static files (default http://localhost:4173). It is not a second dev server: no watchers, no transforms, no HMR, no source maps unless the build emitted them. The workflow to internalize is edit → dev → commit → build → preview — preview is where you find out that a hardcoded /logo.png worked in dev (Vite serves public/ at root) but 404s in production because base was not considered.

npm run test:unit runs Vitest. In watch mode during development; npx vitest run executes once for CI. Tests import your components directly — no browser, because Vitest runs in Node with Vite's transform pipeline, so .vue files import cleanly.

npm run lint runs ESLint with --fix, so it rewrites files rather than only reporting. Run it before committing; configure it in your editor to run on save and the command becomes a CI backstop instead of a habit.

Where the knobs live:

Want to change... File Snippet
An import alias (@/components/...) vite.config.ts + tsconfig paths resolve: { alias: '@': fileURLToPath(new URL('./src', import.meta.url)) }
The dev port or auto-open vite.config.ts server: { port: 3000, strictPort: true }
The URL prefix in production vite.config.ts base: '/app/'
Which files count as globals for ESLint eslint.config.js files: ['**/*.{ts,vue}'] blocks

One habit pays off immediately: run npm run build before you push, every time. It is the same command CI runs, it costs seconds, and it catches template type errors, bad imports, and missing env declarations while the fix is still in your head.

Next, Application Entry and Mounting opens the main.ts that vite executes after index.html loads.

Quiz

1. You scaffold a project and later discover that `npm run build` never catches a misspelled prop in a template expression. Which prompt decision would have prevented that gap?

Question 1 options

2. What does Vite's dev server send the browser on a cold first load?

Question 2 options

3. You save a change to a component's `<script setup>` block and the browser updates while a running counter keeps its value. Which mechanism made that possible?

Question 3 options

4. You run `npm run preview`, edit a component file, and the browser shows no change. Why?

Question 4 options

Flashcards

Question

What does `npm create vue@latest` actually invoke, and what does it deliberately never ask you?

Answer

It invokes create-vue, the Vue team's official scaffolder, which asks only about optional integrations (TypeScript, JSX, Vue Router, Pinia, Vitest, end-to-end testing, ESLint, Prettier). It never asks about the bundler, dev server, or folder layout because the output is always Vite-based with src/, public/, and a root index.html.

Question

Why does Vite's dev server start in roughly constant time regardless of application size?

Answer

It never bundles the app before serving. index.html goes out immediately and every source module is transformed on demand as a native ES module, so only the files the current page imports are processed. Only node_modules dependencies are pre-bundled eagerly, once, with esbuild and cached under node_modules/.vite/deps.

Question

What is dependency pre-bundling, and when does Vite redo it?

Answer

Vite converts CommonJS/UMD packages to ESM and flattens large dependency graphs into single modules in node_modules/.vite/deps so the browser can import them with few requests. esbuild performs the conversion in milliseconds; the cache is invalidated when the lockfile changes, which surfaces as 'optimized dependencies changed. reloading' in the terminal.

Question

What is the difference between `npm run build` and `npm run preview`?

Answer

build runs Rollup, tree-shakes and hashes assets, and writes them to dist/. preview only serves that existing dist/ directory as static files (default port 4173) with no watching, no transforms, and no HMR — it validates the built output, not your source.

Question

In a TypeScript create-vue project, why does the build script read `run-p type-check "build-only {@}" --` instead of plain `vite build`?

Answer

Because esbuild strips types without checking them, `vite build` alone never type-checks. The script runs vue-tsc and vite build in parallel so a template or script type error fails the build — the same gate CI runs. Plain `vite build` (kept as `build-only`) compiles confidently wrong code.

Revision Notes

Key Takeaways

  • 1. create-vue only composes officially supported pieces onto a fixed Vite skeleton — it asks about integrations, never about the bundler or folder layout
  • 2. The dev server serves index.html and transforms modules on demand as native ES modules, so cold-start time barely grows with app size
  • 3. Only node_modules dependencies are pre-bundled (by esbuild, cached in .vite/deps); everything else is compiled lazily and hot-swapped through accept callbacks
  • 4. `vite build` strips types without checking them — the generated `build` script runs vue-tsc in parallel precisely so type errors gate the release
  • 5. `vite preview` serves the existing dist/ with no watchers or HMR, which is why edits appear frozen until you rebuild

Interview Tips

  • • "Why is Vite faster than webpack in dev?" — contrast bundle-first with serve-first: index.html plus on-demand native-ESM transforms, and name esbuild as the Go-based pre-bundler for CommonJS deps
  • • "How does HMR keep my component state?" — the SFC compiles to a module with a hot-accept callback; only the module is swapped while the mounted instance and its refs stay on the live object
  • • "What's the difference between build and preview?" — build produces hashed assets in dist/, preview statically serves them; preview is your last check for absolute-path and base mistakes
  • • "A teammate's build passes with broken types — how?" — they ran `vite build` or a tool that bypasses vue-tsc; esbuild removes types without verifying them, so type-checking must be a separate gate

Cheat Sheet

Scaffolding with create-vue

Bootstrap:

npm create vue@latest   # prompts: TS, JSX, Router, Pinia, Vitest, E2E, ESLint, Prettier
cd my-app && npm install
npm run dev             # http://localhost:5173

Prompt → what it installs:

Answer Installs
TypeScript typescript, vue-tsc, tsconfig files, .ts sources
Vue Router vue-router, src/router/, views/
Pinia pinia, src/stores/
Vitest vitest, @vue/test-utils, test:unit script
ESLint/Prettier eslint + eslint-plugin-vue, lint script

Scripts:

Script Does Writes
dev Vite dev server: on-demand ESM transforms + HMR nothing
build vue-tsc in parallel with vite build (TS preset) dist/
preview static server for dist/ on :4173 nothing
test:unit Vitest (watch; use vitest run in CI) coverage optional
lint eslint . --fix rewrites sources

Dev server facts: cold start serves index.html then transforms modules on request; deps pre-bundled by esbuild into node_modules/.vite/deps (cache keyed off lockfile); HMR swaps modules through accept callbacks so component state survives; port 5173 auto-increments unless strictPort: true.

Config knobs: resolve.alias and server.port/strictPort in vite.config.ts; base for production URL prefix; tsconfig paths must mirror the Vite alias for editor/type resolution.

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading