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.
Dependency Management
We use Bun for package management and as our JavaScript runtime.
Dependency Cooldowns
To prevent instability, catch potential regressions, and mitigate supply chain attacks, we use dependency cooldowns. This ensures that we only update to new package versions after they have been available for at least 7 days, allowing time for malicious or buggy releases to be identified and retracted by the community.
We configure this using bunfig.toml (see Bun documentation):
[install]
# Global cooldown: ignore releases newer than 7 days (in seconds)
minimumReleaseAge = 604800
# These packages bypass the 7-day minimum age requirement (e.g., internal packages)
minimumReleaseAgeExcludes = [
"@types/bun",
"@dcc-bs/audio-recorder.bs.js",
"@dcc-bs/common-ui.bs.js",
"@dcc-bs/communication.bs.js",
"@dcc-bs/event-system.bs.js",
"@dcc-bs/dependency-injection.bs.js"
]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.
