Nuxt.js + TypeScript Coding Standards
These coding standards define how we write, organize, and maintain our Nuxt.js + TypeScript projects.
By following them, we ensure our codebase remains consistent, predictable, and easy to evolve.
Philosophy
Our primary goals are:
- Consistency: Every file should look and behave predictably across the project.
- Clarity: Code should express its intent clearly, without unnecessary complexity.
- Type Safety: Strong typing prevents subtle bugs and simplifies refactoring.
- Maintainability: Future developers should understand the system without relying on tribal knowledge.
- AI-Friendly: Clear code standards help Generative AI tools better understand and generate code that aligns with our project conventions.
All contributors must follow these standards unless explicitly justified with a clear reason.
General Principles
- Do: Write all code in TypeScript with full type coverage.
- Do: Follow the Composition API when writing Vue components.
- Do: Use Biome to lint and format your code before committing.
- Do: Use the new
app/directory for Nuxt applications. - Do: Use Tailwind CSS for styling and Lucide icons for icons.
- Do: Organize and name files consistently to make navigation intuitive.
Naming Conventions
Pages: kebab-case
Do: Write all page file names using lowercase letters separated by dashes.
| Type | Example |
|---|---|
| ✅ Right | pages/user-profile.vue |
| ❌ Wrong | pages/UserProfile.vue, pages/userProfile.vue |
Why: Kebab-case improves readability in URLs and aligns with Nuxt’s default routing system.
Components: PascalCase
Do: Name Vue components in PascalCase — each word starts with an uppercase letter.
| Type | Example |
|---|---|
| ✅ Right | components/UserProfileCard.vue |
| ❌ Wrong | components/userProfileCard.vue, components/user-profile-card.vue |
Why: Vue automatically registers components in PascalCase, which aligns with Vue’s recommended naming conventions for ease of import and recognition.
Other TypeScript Files: camelCase
Do: Name internal logic or utility files in camelCase.
| Type | Example |
|---|---|
| ✅ Right | utils/formatDate.ts |
| ❌ Wrong | utils/format-date.ts, utils/FormatDate.ts |
Why: camelCase is standard for variables and functions in JavaScript and helps visually differentiate them from Vue components or pages.
Server API Files: kebab-case
Do: Write all server API file names using lowercase letters separated by dashes.
| Type | Example |
|---|---|
| ✅ Right | server/api/user-profile.ts |
| ❌ Wrong | server/api/userProfile.ts, server/api/UserProfile.ts |
Why: Kebab-case improves readability in API URLs and maintains consistency with page routing conventions.
Import Aliases
Do: Use Nuxt's built-in aliases for imports to maintain consistent paths across the project.
| Alias | Points To | Example |
|---|---|---|
~/ | /app directory | import { useUser } from '~/composables/useUser' |
~~/ | Root directory | import config from '~~/nuxt.config' |
~~/server | Server directory | import { validateUser } from '~~/server/utils/validation' |
#shared | Shared code (client & server) | import { UserSchema } from '#shared/types/user' |
// ✅ Right
import { useCounter } from '~/composables/useCounter'
import { formatDate } from '~/utils/formatDate'
import { UserService } from '~~/server/services/userService'
import { ApiResponse } from '#shared/types/api'
// ❌ Wrong
import { useCounter } from '../../../composables/useCounter'
import { formatDate } from '../../utils/formatDate'Why: Aliases eliminate brittle relative paths, make imports more readable, and prevent path errors when moving files. They also help Generative AI understand the project structure more clearly.
Reference: Nuxt Aliases Documentation
Typing Rules
- Do: Enforce full TypeScript typing — no implicit
any. - Do: Use
neverfor unreachable code paths. - Do: Use
unknownwhen the type is genuinely uncertain, but always validate before use. - Do not: Use
unknownand then cast it without validation. - Do: Prefer precise, narrow types.
- Do: Define reusable interfaces and types in a
/typesfolder.
// ✅ Right
try {
// ... some code
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}Why: Explicit typing prevents runtime bugs, improves IDE autocomplete, and ensures safety in refactors.
Composition vs Options API
- Do: Use the Composition API exclusively.
- Do not: Use the Options API (
data,methods,computed, etc.).
<!-- ✅ Right -->
<script setup lang="ts">
const count = ref(0);
function increment() {
count.value++;
}
</script>
<!-- ❌ Wrong -->
<script lang="ts">
export default {
data() {
return { count: 0 };
},
methods: {
increment() {
this.count++;
},
},
};
</script>Why: The Composition API provides better type inference, reusability, and scalability for complex logic.
Function Style
- Do: Use standard function declarations (
function foo() {}). - Try not to: Use arrow functions for top-level definitions unless necessary.
// ✅ Right
function formatName(name: string): string {
return name.trim();
}
// ❌ Wrong
const formatName = (name: string): string => name.trim();Why: Named function declarations improve stack traces, readability, and hoisting behavior.
Composables
- Do: Name composables starting with
use. - Do: Each composable should expose a main
use*function that handles logic. - Do: Place composables that depend on Vue reactivity or lifecycle in
/composables. - Try not to: Create a composable for logic that’s independent of Vue; put those in
/utilsor/servicesinstead.
// ✅ Right (in composables/useCounter.ts)
export function useCounter() {
const count = ref(0);
function increment() {
count.value++;
}
return { count, increment };
}
// ❌ Wrong
export const counter = {
count: ref(0),
};Why: The “use” convention makes composables easily discoverable and standardizes how reactive state and lifecycle logic are structured.
Utilities and Services
- Do: Keep framework-agnostic functions in
/utils. - Try: Use
/servicesfor code that’s auto-import–independent or relates to APIs and business logic.
// ✅ utils/formatDate.ts
export function formatDate(date: Date) {
return date.toISOString().split("T");
}
// ✅ services/userServices.ts
export async function fetchUser(id: string) {
return await $fetch(`/api/user/${id}`);
}Why: Separating concerns by folder clarifies function intent, improves reusability, and prevents tight coupling with Nuxt runtime context.
Styling
- Do: Use Tailwind CSS for all styling.
- Do: Use Lucide icons from lucide.dev.
<template>
<div class="flex items-center gap-2 px-4 py-2">
<UButton icon="i-lucide-plus">
Add Item
</UButton>
</div>
</template>Why: Tailwind ensures consistent, responsive UI styling without writing custom CSS. Lucide provides lightweight, modern SVG icons with a unified design language.
Using Biome
- Do: Always run
biome checkbefore pushing code. - Do: Fix all lint and format issues before committing.
# ✅ Right
bun run checkWhy: Biome ensures consistent formatting and code quality across the entire project.
Folder Structure Example
app/
├─ components/
│ └─ UserCard.vue
├─ composables/
│ └─ useUser.ts
├─ pages/
│ └─ user-profile.vue
├─ utils/
│ └─ formatDate.ts
├─ services/
│ └─ userService.ts
└─ types/
└─ user.ts
server/
├─ api/
│ └─ user-account.get.ts
├─ plugins/
│ └─ startupPlugin.ts
└─ middleware
└─ redirectMiddleware.ts
shared/
└─ types/
└─ userTypes.tsWhy: A consistent structure makes the codebase predictable and easy to navigate for all team members.
