№ 02 · Writings · tutorial

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

My Vue starter after years of client projects — TypeScript, Vite, file-based routing, done in minutes.

Published
Read
4m
Contents

After setting up Vue projects from scratch for clients more times than I can count — installing dependencies, configuring ESLint, setting up TypeScript, adding Prettier — I built a streamlined starter that skips the repetitive work.

The Pain Points

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

  1. Adding proper TypeScript configuration that actually works with Vue
  2. Setting up linting rules that don't drive you crazy
  3. Configuring file-based routing because manually defining routes is so 2018
  4. Integrating UI components that don't need endless styling from scratch
  5. Tweaking performance settings you'll forget about until things slow down

Nuxt works for some projects, but it's often more than what's needed. Sometimes a clean Vue setup without the extra abstraction layer is the right call.

The Setup

Step 1: Create the Base Project

Start with Vite:

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

I use Bun for package management — faster and has built-in TypeScript support. npm/yarn work fine too.

Step 2: Add Prettier

bun add -D prettier prettier-plugin-tailwindcss

Create a .prettierrc:

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

Add to package.json:

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

Step 3: ESLint Configuration

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:

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:

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

Step 4: TailwindCSS

bun add tailwindcss @tailwindcss/vite

Update your vite.config.ts:

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 your src/style.css with:

@import "tailwindcss";

Step 5: File-Based Routing

bun add vue-router unplugin-vue-router

Update your vite.config.ts again:

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:

{
  "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:

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

Create src/router.ts:

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:

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:

<template>
  <RouterView />
</template>

Step 6: Add UI Components with shadcn-vue

To avoid reinventing UI components, let's add shadcn-vue:

bunx --bun shadcn-vue@latest init

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

Now let's add a button component:

bunx --bun shadcn-vue@latest add button

Create a page file at src/pages/index.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 (try useRoute("/users/[id]"))
  • Type checking on your components
  • Auto-completion everywhere it matters

File-Based Routing

Create src/pages/about.vue and it's available at /about. Create src/pages/users/[id].vue for a dynamic route at /users/:id. All typed:

<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 (reducing bundle size)

Performance

Performance tracking in dev, stripped in production:

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

Used across multiple client projects:

  • Development speed: New features take ~30% less time to implement
  • Bundle size: ~20% smaller than my previous setups
  • Performance: Consistently scoring 95+ on Lighthouse
  • Maintenance: Much easier to onboard new developers

When to Use This

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 won't slow you down

Skip it if:

  • You need SSR/SSG (use Nuxt)
  • You're working with an existing project with different conventions
  • You prefer a different UI approach than Tailwind

For most client projects, this hits the right balance.

GitHub

pyyupsk/vue-setup