№ 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:
- Adding proper TypeScript configuration that actually works with Vue
- Setting up linting rules that don't drive you crazy
- Configuring file-based routing because manually defining routes is so 2018
- Integrating UI components that don't need endless styling from scratch
- 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 installI 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-tailwindcssCreate 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-vueCreate 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/viteUpdate 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-routerUpdate 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 initWhen prompted, choose your preferred color scheme (I usually go with Neutral).
Now let's add a button component:
bunx --bun shadcn-vue@latest add buttonCreate 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