# Tired of Vue Boilerplate? Here's My Clean, Fast Setup

> This article covers my Vue starter, built after years of client projects. It sets up TypeScript, Vite, and file-based routing in minutes.

I have set up Vue projects from scratch for clients more times than I can count. Eventually, I built a streamlined starter that skips the repeated work. Every project needed the same hour of boilerplate: installing dependencies, configuring ESLint, setting up TypeScript, and adding Prettier, all before writing a single feature.

## The Pain Points

If you have worked with Vue, you know the drill. You run `create-vue` or `vite create`, and then you spend the next hour customizing the setup:

1. You add proper TypeScript configuration that actually works with Vue.
2. You set up linting rules that do not drive you crazy.
3. You configure file-based routing, because manually defining routes feels outdated.
4. You integrate UI components that do not need endless styling from scratch.
5. You tweak performance settings that you will forget about until things slow down.

Nuxt works well for some projects, but it is often more than a project needs. Sometimes a clean Vue setup without the extra abstraction layer is the right choice.

## The Setup

### Step 1: Create the Base Project

Start with Vite:

```bash
bun create vite@latest my-project --template vue-ts
cd my-project
bun install
```

> I use Bun for package management. It runs faster and includes built-in TypeScript support. npm and Yarn also work fine.

### Step 2: Add Prettier

```bash
bun add -D prettier prettier-plugin-tailwindcss
```

Create a `.prettierrc`:

```json
{
  "printWidth": 100,
  "tabWidth": 2,
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "endOfLine": "lf",
  "plugins": ["prettier-plugin-tailwindcss"]
}
```

Add to `package.json`:

```json
"scripts": {
  "format": "prettier --write . --list-different --cache"
}
```

### Step 3: ESLint Configuration

```bash
bun add -D eslint @eslint/js globals typescript-eslint eslint-plugin-perfectionist eslint-plugin-prettier eslint-config-prettier eslint-plugin-vue
```

Create `eslint.config.mjs`:

```javascript
import eslint from "@eslint/js";
import perfectionist from "eslint-plugin-perfectionist";
import prettier from "eslint-plugin-prettier/recommended";
import vue from "eslint-plugin-vue";
import globals from "globals";
import tseslint from "typescript-eslint";

const eslintConfig = tseslint.config(
  eslint.configs.recommended,
  perfectionist.configs["recommended-natural"],
  tseslint.configs.recommended,
  {
    extends: [...vue.configs["flat/recommended"]],
    files: ["**/*.{ts,vue}"],
    languageOptions: {
      ecmaVersion: "latest",
      globals: {
        ...globals.browser,
      },
      parserOptions: {
        parser: tseslint.parser,
      },
      sourceType: "module",
    },
    rules: {
      "no-console": ["warn", { allow: ["warn", "error"] }],
      "vue/multi-word-component-names": "off",
    },
  },
  {
    rules: {
      "@typescript-eslint/consistent-type-definitions": ["error", "type"],
      "@typescript-eslint/consistent-type-imports": [
        "error",
        {
          fixStyle: "separate-type-imports",
          prefer: "type-imports",
        },
      ],
      "@typescript-eslint/no-unused-vars": [
        "warn",
        {
          argsIgnorePattern: "^_",
          ignoreRestSiblings: true,
        },
      ],
    },
  },
  prettier,
);

export default eslintConfig;
```

Add to `package.json`:

```json
"scripts": {
  "lint": "eslint src",
  "lint:fix": "eslint src --fix",
  "typecheck": "vue-tsc --noEmit"
}
```

### Step 4: TailwindCSS

```bash
bun add tailwindcss @tailwindcss/vite
```

Update your `vite.config.ts`:

```typescript
import tailwindcss from "@tailwindcss/vite";
import vue from "@vitejs/plugin-vue";
import path from "node:path";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [vue(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    port: 3000,
    hmr: {
      overlay: true,
    },
  },
});
```

Replace `src/style.css` with:

```css
@import "tailwindcss";
```

### Step 5: File-Based Routing

```bash
bun add vue-router unplugin-vue-router
```

Update `vite.config.ts` again:

```typescript
import tailwindcss from "@tailwindcss/vite";
import vue from "@vitejs/plugin-vue";
import path from "node:path";
import router from "unplugin-vue-router/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    router(), // Must come before vue()
    vue(),
    tailwindcss(),
  ],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    port: 3000,
    hmr: {
      overlay: true,
    },
  },
});
```

Update your TypeScript configuration in `tsconfig.app.json`:

```json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": ["unplugin-vue-router/client"]
  },
  "include": ["src/**/*.ts", "src/**/*.vue", "./typed-router.d.ts"]
}
```

Add type references to `src/vite-env.d.ts`:

```typescript
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />
```

Create `src/router.ts`:

```typescript
import { createRouter, createWebHistory } from "vue-router";
import { routes } from "vue-router/auto-routes";

export const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior(_to, _from, savedPosition) {
    return savedPosition || { top: 0 };
  },
});

export default router;
```

Update `src/main.ts` to use the router:

```typescript
import { createApp } from "vue";

import App from "@/App.vue";
import router from "@/router";
import "@/style.css";

const app = createApp(App);

app.use(router);
app.mount("#app");
```

Update `src/App.vue` to include the router view:

```vue
<template>
  <RouterView />
</template>
```

### Step 6: Add UI Components with shadcn-vue

To avoid reinventing UI components, add shadcn-vue:

```bash
bunx --bun shadcn-vue@latest init
```

When prompted, choose your preferred color scheme (I usually go with Neutral).

Now add a button component:

```bash
bunx --bun shadcn-vue@latest add button
```

Create a page file at `src/pages/index.vue`:

```vue
<script setup lang="ts">
import { Button } from "@/components/ui/button";
</script>

<template>
  <div class="grid h-dvh place-items-center">
    <Button>Clean Setup Complete!</Button>
  </div>
</template>
```

## What This Gets You

### TypeScript That Works

With proper configuration and unplugin-vue-router:

- Fully typed routes, such as `useRoute("/users/[id]")`
- Type checking on your components
- Auto-completion everywhere it matters

### File-Based Routing

Create `src/pages/about.vue`, and it becomes available at `/about`. Create `src/pages/users/[id].vue` for a dynamic route at `/users/:id`. Every route stays fully typed:

```vue
<script setup lang="ts">
import { useRoute } from "vue-router";

// This will be perfectly typed, with route.params.id as a string!
const route = useRoute("/users/[id]");
</script>
```

### UI Components

shadcn-vue provides:

- Accessible components out of the box
- Consistent styling with Tailwind
- Customizable design tokens
- Only the components you need, which reduces bundle size

### Performance

This setup tracks performance during development and strips that tracking in production:

```typescript
import { createApp } from "vue";

import App from "@/App.vue";
import router from "@/router";
import "@/style.css";

const app = createApp(App);

app.config.performance = import.meta.env.DEV;

app.config.compilerOptions = {
  comments: false,
  whitespace: "condense",
};

if (import.meta.env.PROD) {
  app.config.warnHandler = () => null;
}

app.use(router);
app.mount("#app");
```

## Results

I have used this setup across multiple client projects:

- Development speed: about 30% less time per feature
- Bundle size: about 20% smaller than previous setups
- Performance: consistently 95 or higher on Lighthouse
- Maintenance: new developers onboard in a day

## When to Use This

This setup works well if you want:

- A lightweight Vue setup without Nuxt's overhead
- Type safety without the complexity
- Modern file-based routing
- Production-ready performance optimizations
- A component library that will not slow you down

Skip it if:

- You need SSR or SSG, meaning server-side or static-site rendering. Use Nuxt instead.
- You work on an existing project with different conventions.
- You prefer a different UI approach than Tailwind.

For most client projects, this setup hits the right balance.

https://github.com/pyyupsk/vue-setup
