PYYUPSK

a self-taught dev from Thailand

Pongsakorn Thipayanate · Samutsakhon, TH ·webring

tutorial

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.

view .mdopen in claudeopen in chatgpt

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:

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

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

@import "tailwindcss";

Step 5: File-Based Routing

bun add vue-router unplugin-vue-router

Update 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, add shadcn-vue:

bunx --bun shadcn-vue@latest init

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

Now 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, 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:

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

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.

pyyupsk/vue-setupGitHub

A modern Vue.js starter template with Vite, TypeScript, Tailwind CSS, and best practices built-in.

★ 1 · ⑂ 0 · Vue

← All writings