After a couple years of building Vue applications for clients, I've lost count of how many times I've set up new projects from scratch. Each time, I found myself repeating the same steps: installing dependencies, configuring ESLint, setting up TypeScript, adding Prettier, and on and on...
Sound familiar?
That's why I finally took the time to create a streamlined Vue setup that cuts through the noise and lets me focus on what matters: building great applications for clients. Today, I'm sharing this battle-tested setup with you.
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:
Sure, you could use Nuxt—and sometimes I do—but for many projects, it's overkill. Sometimes you just need a clean, fast Vue setup without the extra abstractions.
Here's how I can get a production-ready Vue project running in about 5 minutes. No jokes.
I start with Vite because it's blazing fast:
bun create vite@latest my-project --template vue-ts
cd my-project
bun install
I've switched to using Bun instead of npm/yarn for package management—it's significantly faster and has built-in TypeScript support. But npm/yarn work just fine too!
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"
}
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
with a sensible configuration that works well with Vue, TypeScript, and won't bombard you with warnings:
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"
}
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";
This is where things get good. Let's add automatic file-based routing:
bun add vue-router unplugin-vue-router
Update your vite.config.ts
again:
import router from "unplugin-vue-router/vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite";
import path from "node:path";
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 "@/style.css";
import App from "@/App.vue";
import router from "@/router";
import { createApp } from "vue";
const app = createApp(App);
// Performance optimization
app.config.performance = import.meta.env.DEV;
// Compiler options for better performance
app.config.compilerOptions = {
comments: false,
whitespace: "condense",
};
// Disable development warnings in production
if (import.meta.env.PROD) {
app.config.warnHandler = () => null;
}
app.use(router);
app.mount("#app");
Update src/App.vue
to include the router view:
<template>
<RouterView />
</template>
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-screen place-items-center">
<Button>Clean Setup Complete!</Button>
</div>
</template>
Why do I love this setup so much? Let me count the ways:
No more fighting with Vue's type system. With proper configuration and unplugin-vue-router, you get:
useRoute("/users/[id]")
)Just create a file at src/pages/about.vue
and it's automatically available at /about
. Create src/pages/users/[id].vue
and boom—you have a dynamic route at /users/:id
.
The best part? It's 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>
With shadcn-vue, you get:
The setup includes performance tracking in development mode and strips out unnecessary bloat in production:
// Performance optimization
app.config.performance = import.meta.env.DEV;
// Compiler options for better performance
app.config.compilerOptions = {
comments: false,
whitespace: "condense",
};
// Disable development warnings in production
if (import.meta.env.PROD) {
app.config.warnHandler = () => null;
}
This isn't theoretical—I've used this setup on multiple client projects with clear benefits:
This setup is perfect if you want:
It might not be ideal if:
But for most client projects and applications I build, this setup hits the sweet spot between developer experience and performance.
Want to try it yourself? I've put together a template repository that you can clone and start using immediately.
Let me know what you think in the comments!
What's your preferred Vue setup? I'd love to hear about your approach and any tips you might have!
Related Posts