TypeScript Nuxt.js Basic
A comprehensive Nuxt.js 3 application archetype built with TypeScript, providing a powerful full-stack framework with server-side rendering, static generation, and modern development features for building universal Vue.js applications.
Overview
This archetype generates a complete Nuxt.js 3 application using TypeScript for type safety, Vue.js 3 with Composition API, and comprehensive tooling for building fast, SEO-friendly web applications. It includes both client-side and server-side capabilities with optimal performance and developer experience.
Technology Stack
- Nuxt.js 3: Vue.js-based full-stack framework
- Vue.js 3: Progressive JavaScript framework with Composition API
- TypeScript 5+: Strong typing and modern JavaScript features
- Nitro: Server engine with universal deployment
- Vite: Ultra-fast build tool and development server
- Pinia: State management for Vue.js
- Vitest: Unit testing framework
- Playwright: End-to-end testing
- ESLint: Code linting with Nuxt-specific rules
- Prettier: Code formatting and consistency
- Tailwind CSS: Utility-first CSS framework
Key Features
Universal Rendering
- Server-Side Rendering (SSR): Dynamic server-side rendering
- Static Site Generation (SSG): Pre-rendered static sites
- Incremental Static Regeneration (ISR): Hybrid rendering approach
- Client-Side Rendering (SPA): Single-page application mode
- Universal: Code runs on both client and server
Nuxt.js Features
- Auto Imports: Automatic imports for components, composables, and utilities
- File-based Routing: Automatic route generation from file structure
- Server API: Built-in server-side API routes
- Middleware: Route and global middleware support
- Plugins: Client and server plugin system
- Modules: Rich ecosystem of Nuxt modules
Development Features
- Hot Module Replacement: Fast development with live reloading
- TypeScript Integration: Full TypeScript support throughout
- Dev Tools: Vue DevTools and Nuxt DevTools integration
- Error Handling: Comprehensive error pages and handling
- SEO Optimization: Built-in SEO features and meta management
Project Structure
nuxtjs-app/
├── components/ # Vue components (auto-imported)
│ ├── ui/ # UI components
│ │ ├── UButton.vue
│ │ ├── UInput.vue
│ │ ├── UModal.vue
│ │ └── UCard.vue
│ ├── forms/ # Form components
│ │ ├── ContactForm.vue
│ │ ├── LoginForm.vue
│ │ └── UserForm.vue
│ ├── layout/ # Layout components
│ │ ├── AppHeader.vue
│ │ ├── AppFooter.vue
│ │ ├── AppSidebar.vue
│ │ └── AppNavigation.vue
│ ├── content/ # Content components
│ │ ├── BlogPost.vue
│ │ ├── ProductCard.vue
│ │ └── UserProfile.vue
│ └── global/ # Global components
│ ├── LoadingSpinner.vue
│ ├── ErrorMessage.vue
│ └── Toast.vue
├── pages/ # File-based routing
│ ├── index.vue # Home page (/)
│ ├── about.vue # About page (/about)
│ ├── contact.vue # Contact page (/contact)
│ ├── blog/ # Blog section (/blog)
│ │ ├── index.vue # Blog listing
│ │ ├── [slug].vue # Blog post detail
│ │ └── category/
│ │ └── [name].vue # Blog category
│ ├── users/ # User section (/users)
│ │ ├── index.vue # User listing
│ │ ├── [id].vue # User detail
│ │ ├── profile.vue # User profile
│ │ └── settings.vue # User settings
│ ├── auth/ # Authentication pages
│ │ ├── login.vue # Login page
│ │ ├── register.vue # Registration page
│ │ └── forgot-password.vue # Password reset
│ └── admin/ # Admin section
│ ├── index.vue # Admin dashboard
│ ├── users.vue # User management
│ └── settings.vue # Admin settings
├── layouts/ # Layout templates
│ ├── default.vue # Default layout
│ ├── admin.vue # Admin layout
│ ├── auth.vue # Authentication layout
│ └── minimal.vue # Minimal layout
├── server/ # Server-side code
│ ├── api/ # API routes
│ │ ├── auth/ # Authentication API
│ │ │ ├── login.post.ts
│ │ │ ├── register.post.ts
│ │ │ └── logout.post.ts
│ │ ├── users/ # User API
│ │ │ ├── index.get.ts # GET /api/users
│ │ │ ├── index.post.ts # POST /api/users
│ │ │ └── [id].get.ts # GET /api/users/:id
│ │ ├── blog/ # Blog API
│ │ │ ├── posts.get.ts
│ │ │ └── [slug].get.ts
│ │ └── health.get.ts # Health check endpoint
│ ├── middleware/ # Server middleware
│ │ ├── auth.ts # Authentication middleware
│ │ ├── cors.ts # CORS middleware
│ │ └── logging.ts # Request logging
│ └── utils/ # Server utilities
│ ├── db.ts # Database utilities
│ ├── auth.ts # Auth utilities
│ └── validation.ts # Validation utilities
├── composables/ # Composition functions (auto-imported)
│ ├── useAuth.ts # Authentication composable
│ ├── useApi.ts # API composable
│ ├── useBlog.ts # Blog composable
│ ├── useUsers.ts # Users composable
│ ├── usePagination.ts # Pagination composable
│ ├── useValidation.ts # Validation composable
│ └── useTheme.ts # Theme composable
├── stores/ # Pinia stores (auto-imported)
│ ├── auth.ts # Authentication store
│ ├── user.ts # User store
│ ├── blog.ts # Blog store
│ └── ui.ts # UI state store
├── plugins/ # Nuxt plugins
│ ├── pinia.client.ts # Pinia setup (client)
│ ├── api.client.ts # API client setup
│ ├── toast.client.ts # Toast notifications
│ └── auth.client.ts # Auth initialization
├── middleware/ # Route middleware
│ ├── auth.ts # Authentication middleware
│ ├── admin.ts # Admin middleware
│ ├── guest.ts # Guest-only middleware
│ └── redirect.global.ts # Global redirect middleware
├── utils/ # Utility functions (auto-imported)
│ ├── validation.ts # Validation helpers
│ ├── formatters.ts # Data formatters
│ ├── constants.ts # App constants
│ ├── helpers.ts # Helper functions
│ └── types.ts # Type utilities
├── types/ # TypeScript type definitions
│ ├── auth.ts # Authentication types
│ ├── user.ts # User types
│ ├── blog.ts # Blog types
│ ├── api.ts # API response types
│ └── global.ts # Global types
├── assets/ # Assets to be processed
│ ├── css/ # CSS files
│ │ ├── main.css # Main styles
│ │ ├── components.css # Component styles
│ │ └── utilities.css # Utility classes
│ ├── images/ # Images
│ └── icons/ # Icons
├── public/ # Static files
│ ├── favicon.ico
│ ├── robots.txt
│ ├── manifest.json
│ └── images/
├── tests/ # Test files
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── e2e/ # End-to-end tests
├── docs/ # Documentation
├── .github/ # GitHub Actions workflows
├── nuxt.config.ts # Nuxt configuration
├── app.config.ts # App configuration
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── tailwind.config.js # Tailwind CSS configuration
├── playwright.config.ts # Playwright configuration
└── README.md # Project documentation
Nuxt.js Configuration
Main Configuration
// nuxt.config.ts
export default defineNuxtConfig({
// Modern Nuxt defaults
devtools: { enabled: true },
// TypeScript configuration
typescript: {
strict: true,
typeCheck: true
},
// CSS framework
css: [
'@/assets/css/main.css'
],
// Modules
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@nuxt/content',
'@nuxtjs/google-fonts',
'@vueuse/nuxt'
],
// Runtime config
runtimeConfig: {
// Private keys (only available on server-side)
jwtSecret: process.env.JWT_SECRET,
databaseUrl: process.env.DATABASE_URL,
// Public keys (exposed to client-side)
public: {
apiBase: process.env.API_BASE_URL || '/api',
appName: process.env.APP_NAME || 'Nuxt App',
appVersion: process.env.APP_VERSION || '1.0.0'
}
},
// App configuration
app: {
head: {
title: 'Nuxt TypeScript App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: 'Modern Nuxt.js application with TypeScript' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
},
// Server-side rendering
ssr: true,
// Route generation for static sites
nitro: {
prerender: {
routes: ['/sitemap.xml', '/robots.txt']
}
},
// Build configuration
build: {
transpile: []
},
// Vite configuration
vite: {
optimizeDeps: {
include: []
}
}
})
Server API Implementation
Authentication API
// server/api/auth/login.post.ts
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(6)
})
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const { email, password } = loginSchema.parse(body)
// In a real app, fetch user from database
const user = await findUserByEmail(email)
if (!user) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid credentials'
})
}
// Verify password
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid credentials'
})
}
// Generate JWT token
const config = useRuntimeConfig()
const token = jwt.sign(
{ userId: user.id, email: user.email },
config.jwtSecret,
{ expiresIn: '7d' }
)
// Set secure HTTP-only cookie
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7 // 7 days
})
return {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
},
token
}
} catch (error) {
if (error instanceof z.ZodError) {
throw createError({
statusCode: 400,
statusMessage: 'Validation error',
data: error.errors
})
}
throw error
}
})
// server/utils/auth.ts
import jwt from 'jsonwebtoken'
import type { User } from '~/types/auth'
export async function findUserByEmail(email: string): Promise<User | null> {
// Implementation depends on your database
// This is a mock implementation
const users = [
{
id: '1',
email: 'admin@example.com',
name: 'Admin User',
role: 'admin',
passwordHash: await bcrypt.hash('password123', 10)
}
]
return users.find(user => user.email === email) || null
}
export function verifyToken(token: string): any {
const config = useRuntimeConfig()
try {
return jwt.verify(token, config.jwtSecret)
} catch {
return null
}
}
User API
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
// Verify authentication
const token = getCookie(event, 'auth-token')
if (!token || !verifyToken(token)) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized'
})
}
// Get query parameters
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 10
const search = String(query.search || '')
try {
// In a real app, fetch from database with pagination
const users = await getUsersWithPagination({ page, limit, search })
return {
data: users.data,
pagination: {
page: users.currentPage,
limit: users.pageSize,
total: users.totalCount,
totalPages: users.totalPages
}
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch users'
})
}
})
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const userId = getRouterParam(event, 'id')
if (!userId) {
throw createError({
statusCode: 400,
statusMessage: 'User ID is required'
})
}
// Verify authentication
const token = getCookie(event, 'auth-token')
if (!token || !verifyToken(token)) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized'
})
}
try {
const user = await findUserById(userId)
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
return {
data: user
}
} catch (error) {
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch user'
})
}
})
Composables
Authentication Composable
// composables/useAuth.ts
import type { User, LoginCredentials, RegisterData } from '~/types/auth'
export const useAuth = () => {
const user = useState<User | null>('auth.user', () => null)
const isLoggedIn = computed(() => !!user.value)
const login = async (credentials: LoginCredentials) => {
const { data } = await $fetch<{ user: User; token: string }>('/api/auth/login', {
method: 'POST',
body: credentials
})
user.value = data.user
// Navigate to dashboard or redirect URL
const redirect = useRoute().query.redirect as string
await navigateTo(redirect || '/dashboard')
return data
}
const register = async (userData: RegisterData) => {
const { data } = await $fetch<{ user: User; token: string }>('/api/auth/register', {
method: 'POST',
body: userData
})
user.value = data.user
await navigateTo('/dashboard')
return data
}
const logout = async () => {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
await navigateTo('/login')
}
const fetchUser = async () => {
try {
const { data } = await $fetch<{ user: User }>('/api/auth/me')
user.value = data.user
} catch (error) {
user.value = null
}
}
const hasRole = (role: string): boolean => {
return user.value?.role === role
}
const hasPermission = (permission: string): boolean => {
return user.value?.permissions?.includes(permission) ?? false
}
return {
user: readonly(user),
isLoggedIn,
login,
register,
logout,
fetchUser,
hasRole,
hasPermission
}
}
API Composable
// composables/useApi.ts
interface UseApiOptions {
server?: boolean
default?: () => any
transform?: (data: any) => any
pick?: string[]
watch?: WatchSource[]
immediate?: boolean
}
export const useApi = <T = any>(
url: string | (() => string),
options: UseApiOptions = {}
) => {
const {
server = true,
default: defaultValue,
transform,
pick,
watch,
immediate = true
} = options
const data = ref<T | null>(null)
const pending = ref(false)
const error = ref<Error | null>(null)
const execute = async (): Promise<T | null> => {
try {
pending.value = true
error.value = null
const requestUrl = typeof url === 'function' ? url() : url
let result = await $fetch<T>(requestUrl)
if (transform) {
result = transform(result)
}
if (pick && Array.isArray(pick)) {
result = pick.reduce((acc, key) => {
if (result && typeof result === 'object' && key in result) {
acc[key] = (result as any)[key]
}
return acc
}, {} as any)
}
data.value = result
return result
} catch (err) {
error.value = err instanceof Error ? err : new Error('Unknown error')
return null
} finally {
pending.value = false
}
}
const refresh = () => execute()
// Auto-execute on mount if immediate is true
if (immediate) {
if (process.server && server) {
execute()
} else if (process.client) {
onMounted(() => execute())
}
}
// Watch dependencies
if (watch && Array.isArray(watch)) {
watchEffect(() => {
if (watch.some(source => unref(source))) {
execute()
}
})
}
return {
data: readonly(data),
pending: readonly(pending),
error: readonly(error),
execute,
refresh
}
}
User Management Composable
// composables/useUsers.ts
import type { User, CreateUserRequest, UpdateUserRequest, UserFilters } from '~/types/user'
export const useUsers = () => {
const users = ref<User[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const pagination = ref({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
const fetchUsers = async (filters: UserFilters = {}) => {
loading.value = true
error.value = null
try {
const query = new URLSearchParams({
page: pagination.value.page.toString(),
limit: pagination.value.limit.toString(),
...(filters.search && { search: filters.search }),
...(filters.role && { role: filters.role }),
...(filters.status && { status: filters.status })
})
const response = await $fetch<{
data: User[]
pagination: typeof pagination.value
}>(`/api/users?${query}`)
users.value = response.data
pagination.value = response.pagination
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch users'
} finally {
loading.value = false
}
}
const createUser = async (userData: CreateUserRequest): Promise<User | null> => {
loading.value = true
error.value = null
try {
const response = await $fetch<{ data: User }>('/api/users', {
method: 'POST',
body: userData
})
users.value.push(response.data)
return response.data
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to create user'
return null
} finally {
loading.value = false
}
}
const updateUser = async (id: string, userData: UpdateUserRequest): Promise<User | null> => {
loading.value = true
error.value = null
try {
const response = await $fetch<{ data: User }>(`/api/users/${id}`, {
method: 'PUT',
body: userData
})
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value[index] = response.data
}
return response.data
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to update user'
return null
} finally {
loading.value = false
}
}
const deleteUser = async (id: string): Promise<boolean> => {
loading.value = true
error.value = null
try {
await $fetch(`/api/users/${id}`, { method: 'DELETE' })
users.value = users.value.filter(user => user.id !== id)
return true
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to delete user'
return false
} finally {
loading.value = false
}
}
const nextPage = () => {
if (pagination.value.page < pagination.value.totalPages) {
pagination.value.page++
fetchUsers()
}
}
const previousPage = () => {
if (pagination.value.page > 1) {
pagination.value.page--
fetchUsers()
}
}
const goToPage = (page: number) => {
if (page >= 1 && page <= pagination.value.totalPages) {
pagination.value.page = page
fetchUsers()
}
}
return {
users: readonly(users),
loading: readonly(loading),
error: readonly(error),
pagination: readonly(pagination),
fetchUsers,
createUser,
updateUser,
deleteUser,
nextPage,
previousPage,
goToPage
}
}
Component Examples
Page Component
<!-- pages/users/index.vue -->
<template>
<div class="users-page">
<Head>
<Title>Users - Nuxt App</Title>
<Meta name="description" content="Manage application users" />
</Head>
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-900">Users</h1>
<UButton @click="showCreateModal = true">
Add New User
</UButton>
</div>
<!-- Search and Filters -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<UInput
v-model="filters.search"
placeholder="Search users..."
@input="debouncedSearch"
/>
<USelect
v-model="filters.role"
:options="roleOptions"
placeholder="Filter by role"
@change="fetchUsers(filters)"
/>
<USelect
v-model="filters.status"
:options="statusOptions"
placeholder="Filter by status"
@change="fetchUsers(filters)"
/>
</div>
<!-- Loading State -->
<div v-if="loading" class="text-center py-8">
<LoadingSpinner />
<p class="mt-2 text-gray-600">Loading users...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-8">
<ErrorMessage :message="error" />
<UButton @click="fetchUsers(filters)" class="mt-4">
Try Again
</UButton>
</div>
<!-- Users List -->
<div v-else-if="users.length > 0" class="space-y-4">
<UserCard
v-for="user in users"
:key="user.id"
:user="user"
@edit="editUser"
@delete="deleteUser"
/>
<!-- Pagination -->
<div class="flex justify-center mt-8">
<UPagination
:current-page="pagination.page"
:total-pages="pagination.totalPages"
@page-change="goToPage"
/>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-12">
<div class="text-gray-400 mb-4">
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No users found</h3>
<p class="text-gray-600 mb-4">Get started by creating your first user.</p>
<UButton @click="showCreateModal = true">
Add New User
</UButton>
</div>
</div>
<!-- Create/Edit Modal -->
<UModal v-model="showCreateModal" title="Create User">
<UserForm
:initial-data="editingUser"
@submit="handleUserSubmit"
@cancel="closeModal"
/>
</UModal>
</div>
</template>
<script setup lang="ts">
import { debounce } from 'lodash-es'
import type { User, UserFilters } from '~/types/user'
// Define page meta
definePageMeta({
middleware: 'auth',
layout: 'default'
})
// Composables
const { users, loading, error, pagination, fetchUsers, createUser, updateUser, deleteUser, goToPage } = useUsers()
// Reactive data
const showCreateModal = ref(false)
const editingUser = ref<User | null>(null)
const filters = reactive<UserFilters>({
search: '',
role: '',
status: ''
})
// Options for selects
const roleOptions = [
{ label: 'All Roles', value: '' },
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
{ label: 'Manager', value: 'manager' }
]
const statusOptions = [
{ label: 'All Status', value: '' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Pending', value: 'pending' }
]
// Debounced search
const debouncedSearch = debounce(() => {
fetchUsers(filters)
}, 300)
// Methods
const editUser = (user: User) => {
editingUser.value = user
showCreateModal.value = true
}
const handleUserSubmit = async (userData: any) => {
if (editingUser.value) {
await updateUser(editingUser.value.id, userData)
} else {
await createUser(userData)
}
closeModal()
}
const closeModal = () => {
showCreateModal.value = false
editingUser.value = null
}
const confirmDelete = async (user: User) => {
const confirmed = confirm(`Are you sure you want to delete ${user.name}?`)
if (confirmed) {
await deleteUser(user.id)
}
}
// Initialize data
onMounted(() => {
fetchUsers(filters)
})
// SEO
useSeoMeta({
title: 'Users Management',
description: 'Manage application users, roles, and permissions',
ogTitle: 'Users Management - Nuxt App',
ogDescription: 'Manage application users, roles, and permissions'
})
</script>
Middleware
Authentication Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useAuth()
if (!isLoggedIn.value) {
return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
}
})
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { user, hasRole } = useAuth()
if (!user.value || !hasRole('admin')) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden: Admin access required'
})
}
})
// middleware/guest.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useAuth()
if (isLoggedIn.value) {
return navigateTo('/dashboard')
}
})
Testing Strategy
Component Testing
// tests/unit/components/UserCard.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import UserCard from '~/components/UserCard.vue'
import type { User } from '~/types/user'
const mockUser: User = {
id: '1',
name: 'John Doe',
email: 'john.doe@example.com',
role: 'user',
status: 'active',
createdAt: '2023-01-01T00:00:00Z'
}
describe('UserCard', () => {
it('renders user information correctly', () => {
const wrapper = mount(UserCard, {
props: { user: mockUser }
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john.doe@example.com')
expect(wrapper.text()).toContain('user')
expect(wrapper.text()).toContain('active')
})
it('emits edit event when edit button is clicked', async () => {
const wrapper = mount(UserCard, {
props: { user: mockUser }
})
await wrapper.find('[data-testid="edit-button"]').trigger('click')
expect(wrapper.emitted('edit')).toBeTruthy()
expect(wrapper.emitted('edit')![0]).toEqual([mockUser])
})
it('emits delete event when delete button is clicked', async () => {
const wrapper = mount(UserCard, {
props: { user: mockUser }
})
await wrapper.find('[data-testid="delete-button"]').trigger('click')
expect(wrapper.emitted('delete')).toBeTruthy()
expect(wrapper.emitted('delete')![0]).toEqual([mockUser])
})
})
API Testing
// tests/unit/server/api/users.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createEvent } from 'h3'
import handler from '~/server/api/users/index.get'
// Mock dependencies
vi.mock('~/server/utils/auth', () => ({
verifyToken: vi.fn(),
getUsersWithPagination: vi.fn()
}))
describe('/api/users', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should return users when authenticated', async () => {
const mockUsers = {
data: [
{ id: '1', name: 'John Doe', email: 'john@example.com' }
],
currentPage: 1,
pageSize: 10,
totalCount: 1,
totalPages: 1
}
vi.mocked(verifyToken).mockReturnValue({ userId: '1' })
vi.mocked(getUsersWithPagination).mockResolvedValue(mockUsers)
const event = createEvent({
method: 'GET',
url: '/api/users',
headers: {
cookie: 'auth-token=valid-token'
}
})
const result = await handler(event)
expect(result.data).toEqual(mockUsers.data)
expect(result.pagination.total).toBe(1)
})
it('should return 401 when not authenticated', async () => {
vi.mocked(verifyToken).mockReturnValue(null)
const event = createEvent({
method: 'GET',
url: '/api/users'
})
await expect(handler(event)).rejects.toThrow('Unauthorized')
})
})
E2E Testing
// tests/e2e/user-management.spec.ts
import { test, expect } from '@playwright/test'
test.describe('User Management', () => {
test.beforeEach(async ({ page }) => {
// Login
await page.goto('/login')
await page.fill('[data-testid="email-input"]', 'admin@example.com')
await page.fill('[data-testid="password-input"]', 'password123')
await page.click('[data-testid="login-button"]')
// Navigate to users page
await page.goto('/users')
})
test('should display list of users', async ({ page }) => {
await expect(page.locator('h1')).toContainText('Users')
await expect(page.locator('[data-testid="user-card"]')).toHaveCountGreaterThan(0)
})
test('should create a new user', async ({ page }) => {
await page.click('[data-testid="add-user-button"]')
await page.fill('[data-testid="name-input"]', 'Test User')
await page.fill('[data-testid="email-input"]', 'test@example.com')
await page.selectOption('[data-testid="role-select"]', 'user')
await page.click('[data-testid="submit-button"]')
await expect(page.locator('text=Test User')).toBeVisible()
})
test('should search users', async ({ page }) => {
await page.fill('[data-testid="search-input"]', 'John')
await expect(page.locator('[data-testid="user-card"]')).toHaveCount(1)
await expect(page.locator('text=John')).toBeVisible()
})
test('should filter users by role', async ({ page }) => {
await page.selectOption('[data-testid="role-filter"]', 'admin')
const userCards = page.locator('[data-testid="user-card"]')
const count = await userCards.count()
for (let i = 0; i < count; i++) {
await expect(userCards.nth(i)).toContainText('admin')
}
})
})
Quick Start
-
Generate the application:
archetect render git@github.com:p6m-archetypes/typescript-nuxtjs-basic.archetype.git -
Install dependencies:
npm install -
Set up environment variables:
cp .env.example .env
# Edit .env with your configuration -
Start development server:
npm run dev -
Access the application:
http://localhost:3000 -
Run tests:
# Unit tests
npm run test
# E2E tests
npm run test:e2e
# Test coverage
npm run test:coverage -
Build for production:
npm run build -
Preview production build:
npm run preview
Best Practices
Universal Rendering
- Use
process.serverandprocess.clientfor environment-specific code - Implement proper hydration strategies
- Handle SSR/client-side differences gracefully
- Use
useStatefor reactive server-side state
Performance
- Implement proper lazy loading for components and pages
- Use Nuxt's automatic code splitting
- Optimize images and assets
- Implement proper caching strategies
SEO
- Use
useSeoMetafor dynamic meta tags - Implement structured data with JSON-LD
- Create proper sitemaps and robots.txt
- Use semantic HTML elements
Security
- Validate all inputs on both client and server
- Use secure HTTP-only cookies for authentication
- Implement proper CORS configuration
- Sanitize user-generated content