Getting Started with Vue 3: From npm create vue to Understanding script setup

Getting Started with Vue 3 tutorial covering npm create vue and script setup
Getting started with Vue 3, from creating a new project to understanding script setup.

If you’re brand new to Vue and just ran your first npm create vue@latest command, congratulations — you’ve officially joined the club of developers staring at a terminal wondering what half the prompts mean. Don’t worry, that’s normal. Vue’s scaffolding tool asks a lot of questions for something that’s supposed to be “quick start.”

In this post, we’ll walk through two things: how to actually get a new Vue project up and running, and what’s going on inside that mysterious <script setup> block once you open your first component file.

Scaffolding a New Vue Project

Vue’s official scaffolding tool is called create-vue, and you kick it off with:

npm create vue@latest

This tells npm: “go fetch the latest version of the Vue project generator and run it.” If you ever need a specific version instead of whatever is newest, just swap latest for the version number:

npm create vue@3.17.0

Pinning a version like this is handy if you’re following a tutorial (like this one!) and want your setup to match exactly, rather than getting surprised by a newer version that changed the prompts on you.

The Project Name Prompt

The first thing the tool asks is:

Project name (target directory):

This is asking where to put your new project. If you type a name, it creates a new folder with that name. If you’re already sitting inside the folder you want to use, you can just type a single period (.) to mean “right here.”

If that folder already has files in it, you’ll get a follow-up question:

Current directory is not empty. Remove existing files and continue?

Answering “Yes” wipes out whatever’s currently in that folder and starts fresh. Answer carefully here — this step is basically npm asking “are you sure?” before it deletes things, so don’t be the person who says yes without checking first.

The Package Name Prompt (and Why It Rejects You)

Next up:

Package name:

This one trips up a lot of beginners, mostly because the error message it throws — Invalid package.json name — doesn’t explain what actually went wrong. Here’s the deal: this value becomes the name field inside your project’s package.json file, and npm package names have some strict formatting rules. They’re picky in a way that feels oddly personal:

  • All lowercase — no capital letters allowed
  • No spaces
  • Only letters, numbers, hyphens (-), and underscores (_)
  • Can’t start with a dot or an underscore

So if you typed something like “My Vue App” or hit Enter with nothing typed at all, that’s why it rejected you. The fix is simple — just type a valid, lowercase, hyphenated name, like:

learn-vue

This name is really just metadata. It doesn’t get published anywhere unless you later run npm publish, and it has zero effect on how your app actually runs. For a personal or learning project, it genuinely does not matter what you call it — just get past the prompt.

The Rest of the Setup Questions

After that, create-vue walks you through a series of yes/no feature toggles, such as:

  • Add TypeScript?
  • Add JSX support?
  • Add Vue Router (for multi-page navigation)?
  • Add Pinia (for state management)?
  • Add Vitest (for unit testing)?
  • Add an End-to-End testing solution?
  • Add ESLint (for catching code issues)?
  • Add Prettier (for auto-formatting code)?

As a beginner, it’s completely fine to say no to most of these and add them later once you know you actually need them. There’s no prize for enabling everything on your first try.

Installing and Running Your App

Once the scaffolding finishes, move into your project folder, install the dependencies, and start the dev server:

cd learn-vue
npm install
npm run dev

Vite (the tool powering Vue’s dev server) will spin up a local server and give you a URL to open in your browser, usually something like http://localhost:5173/. If that port happens to be busy, Vite will simply try the next one — 5174, 5175, and so on — until it finds one that’s free. Just use whichever URL shows up in your terminal.

Understanding <script setup>

Vue 3 script setup example showing ref, reactivity, createApp, and Vue Composition API concepts
A visual introduction to Vue 3 concepts including script setup, ref(), createApp(), and reactive state.

Once your project is running, open up a component file and you’ll likely see something like this:

<script setup>
import { ref } from 'vue'

const name = ref('Mark')

import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
</script>

<script setup> is a special shorthand syntax for Vue 3’s Composition API. It exists purely to save you from typing a bunch of boilerplate. Here’s the “long way” of writing the exact same logic:

<script>
import { ref } from 'vue'

export default {
  setup() {
    const name = ref('Mark')
    return { name } // manually expose it to the template
  }
}
</script>

Both versions do the same thing, but <script setup> skips two annoying steps:

  1. You don’t need to manually return every variable you want your template to see — anything declared at the top level is automatically available.
  2. Imported components (like Header and Footer above) are automatically registered — no need for a separate components: { Header, Footer } object.

So in the example, name, Header, and Footer are all immediately usable inside your <template>:

<template>
  <Header />
  <p>Hello, {{ name }}</p>
  <Footer />
</template>

What’s Actually Happening Under the Hood: createApp and setup()

If you’ve seen Vue used without a build tool (say, loaded straight from a CDN), you might run into code like this instead:

const { createApp, ref } = Vue

createApp({
    setup() {
        const emoji = ref("🌼")
        const status_code = ref("500")
        const status_message = ref("Server is sleeping.")

        return { emoji, status_code, status_message }
    }
}).mount("#app")

This looks like a lot at once, so let’s pull it apart layer by layer.

1. Pulling tools out of Vue

const { createApp, ref } = Vue

This grabs two things out of the Vue library: createApp, which builds a Vue application, and ref, which makes a value “reactive” — meaning Vue watches it for changes.

2. The nesting, unpacked

The part that confuses most beginners is this structure:

createApp({
    setup() {
        // code here
    }
}).mount("#app")

Strip away the Vue-specific meaning, and the shape is just this generic pattern:

someFunction({
    key: function() { ... }
}).anotherFunction()

In other words: call createApp(...), pass it one argument — a configuration object — and that object has a property called setup whose value is a function. createApp(...) then returns something, and you immediately call .mount("#app") on whatever got returned.

setup isn’t some independent, floating function. It’s just a property on the config object, the same way emoji is a property on { emoji, status_code, status_message } later in the code. Vue’s config object can hold other properties too, like components or methodssetup just happens to be where your data and logic live.

3. Why .mount() comes right after

This is called chaining. createApp({ ... }) doesn’t just run and disappear into the void — it returns an app object, and that object has its own .mount() method sitting on it. Written out separately, it looks like this:

const myApp = createApp({
    setup() {
        // ...
    }
})

myApp.mount("#app")

createApp builds the app and hands it back to you. .mount is a separate step you then call on that result. They’re just squished onto one line for convenience.

4. Reactive variables with ref()

const emoji = ref("🌼")
const status_code = ref("500")
const status_message = ref("Server is sleeping.")

ref(...) wraps a plain value in a special reactive container — think of it as a labeled box that Vue keeps a close eye on. If the value inside ever changes, Vue automatically updates every place that value is displayed on the page. No manual DOM updates, no document.getElementById nonsense.

5. Returning data to the template

return { emoji, status_code, status_message }

This is the connector between your logic and your HTML. Whatever you return from setup() becomes available in the template, which is why you can write:

<span>{{ emoji }}</span>
<h1>{{ status_code }}</h1>

Those double curly braces are Vue’s way of saying “put the value of this variable here.” Right now, none of these values change, so you won’t actually see the reactivity in action — but if a button later updated status_code.value, the <h1> on the page would update instantly, with zero additional code required.

Wrapping Up

In plain English, all of this together says: create a Vue app, give it some reactive data, hand that data to the HTML so it can be displayed, and attach the whole thing to a specific spot in the page. <script setup> is just Vue’s way of letting you skip the ceremony and get straight to the good part — writing logic that actually does something.

Once this clicks, the rest of Vue starts to feel a lot less like memorizing magic incantations and a lot more like, well, just JavaScript with some very helpful reactivity built in.