8 min read
Building Type-Safe APIs with Nuxt 3 and Prisma
Learn how to build end-to-end type-safe APIs using Nuxt 3 server routes and Prisma ORM for a seamless developer experience.
1.9K
Implement secure authentication in SPAs with token rotation, silent refresh, secure storage, and protection against common attacks.
Mbeah Essilfie
April 18, 2026 at 06:04 AM
SPAs run in the browser — an inherently untrusted environment. Tokens stored in JavaScript are vulnerable to XSS. Cookies have CSRF risks. Here's how to navigate this securely.
// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
const user = await authenticateUser(email, password)
if (!user) throw createError({ statusCode: 401 })
const { accessToken, refreshToken } = generateTokens(user)
// Set tokens as HttpOnly cookies — inaccessible to JavaScript
setCookie(event, 'access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 900, // 15 minutes
path: '/',
})
setCookie(event, 'refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 604800, // 7 days
path: '/api/auth/refresh', // Only sent to refresh endpoint
})
return { user: { id: user.id, name: user.name, email: user.email } }
})
// server/api/auth/refresh.post.ts
export default defineEventHandler(async (event) => {
const refreshToken = getCookie(event, 'refresh_token')
if (!refreshToken) throw createError({ statusCode: 401 })
try {
const payload = verifyRefreshToken(refreshToken)
const user = await getUser(payload.sub)
// Rotate refresh token (one-time use)
const newTokens = generateTokens(user)
setCookie(event, 'access_token', newTokens.accessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900, path: '/',
})
setCookie(event, 'refresh_token', newTokens.refreshToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 604800, path: '/api/auth/refresh',
})
return { user: { id: user.id, name: user.name } }
} catch {
// Invalid refresh token — force re-login
deleteCookie(event, 'access_token')
deleteCookie(event, 'refresh_token')
throw createError({ statusCode: 401 })
}
})
// composables/useAuth.ts
export function useAuth() {
const user = useState<User | null>('auth-user', () => null)
const isAuthenticated = computed(() => !!user.value)
async function login(email: string, password: string) {
const { user: userData } = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password },
})
user.value = userData
}
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
navigateTo('/login')
}
async function refreshSession() {
try {
const { user: userData } = await $fetch('/api/auth/refresh', { method: 'POST' })
user.value = userData
} catch {
user.value = null
}
}
return { user, isAuthenticated, login, logout, refreshSession }
}
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to) => {
const { isAuthenticated, refreshSession } = useAuth()
if (!isAuthenticated.value) {
await refreshSession()
}
if (!isAuthenticated.value && to.meta.requiresAuth) {
return navigateTo(`/login?redirect=${to.fullPath}`)
}
})
| Attack | Mitigation |
|---|---|
| XSS stealing tokens | HttpOnly cookies (JS can't read them) |
| CSRF | SameSite=Lax + CSRF tokens for mutations |
| Token theft | Short expiry + rotation |
| Brute force | Rate limiting + account lockout |
| Session fixation | Generate new session on login |
Fullstack Software Developer
Learn how to build end-to-end type-safe APIs using Nuxt 3 server routes and Prisma ORM for a seamless developer experience.
Go beyond basic utility classes and learn how to build cohesive, maintainable design systems with Tailwind CSS.
Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.
