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:
- You add proper TypeScript configuration that actually works with Vue.
- You set up linting rules that do not drive you crazy.
- You configure file-based routing, because manually defining routes feels outdated.
- You integrate UI components that do not need endless styling from scratch.
- 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.
A modern Vue.js starter template with Vite, TypeScript, Tailwind CSS, and best practices built-in.
★ 1 · ⑂ 0 · Vue