Full Stack Development with Nuxt
Introduction
Nuxt's initial goal is to create web applications with server-side rendering capabilities. It uses Node.js, meaning we can also create API routes as long as you run Nuxt with a Node.js server.
We have been thinking about this with @pi0 for a long time back in Nuxt 2. However, updating the server would restart the whole Nuxt application and we had to wait for the Vue build to finish which was excessively time consuming.
With Nuxt 3 and Nitro, we opened the possibility to run Nuxt on serverless environments (AWS Lambda) but also on the edge. We also improved the developer experience by creating the possibility to write server code without having to restart the whole Nuxt application. This gives a "hot module replacement" feeling on the server 🚀.
API Routes
The API routes follow a similar convention as the pages/
directory in Nuxt. All you have to do is to create a file in the server/api/
directory:
export default defineEventHandler((event) => {
return { hello: 'world' }
})
defineEventHandler
comes from unjs/h3, the HTTP framework used by Nuxt.When running your Nuxt server with nuxt dev
, you can call the API route by opening http://localhost:3000/api/hello in your browser.
If you have the Nuxt DevTools enabled, we created a Postman-like UI for you to call your API routes (Server Routes tab):
If you want to match a specific HTTP Method, you can suffix the filename with .get
, .post
, .put
, .delete
, etc.
export default defineEventHandler(() => 'Only GET handler')
API Route Validation
The v1.8 release of H3 added methods to validate the params/query/body of the incoming request to make sure you deal with correct values in your handlers.
For the example, we are going to use the popular zod library to validate the body of our /api/login
handler:
import { z } from 'zod'
const bodySchema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, bodySchema.parse)
return { body: 'valid' }
})
zod
dependency in your project (npm i zod
).If you send a request with an invalid email address or password, the server will respond with a 400 status code automatically:
Adding Authentication
Next, we want to set a user session if the login credentials are valid so we can know in our Nuxt app if the user is authenticated.
Let's add nuxt-auth-utils for this:
npx nuxi@latest module add auth-utils
nuxt-auth-utils
as dependency and push it in the modules
section of our nuxt.config.ts
Add a NUXT_SESSION_PASSWORD env variable with at least 32 characters in the .env.
NUXT_SESSION_PASSWORD=password-with-at-least-32-characters
NUXT_SESSION_PASSWORD
environment variable is defined when starting your development server, it will generate one and add it to your .env
.We can update our /api/login
handler to set the user session if it matches some static credentials:
import { z } from 'zod'
const bodySchema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export default defineEventHandler(async (event) => {
const { email, password } = await readValidatedBody(event, bodySchema.parse)
if (email === 'admin@admin.com' && password === 'iamtheadmin') {
await setUserSession(event, {
user: {
name: 'John Doe'
}
})
return {}
}
throw createError({
statusCode: 401,
message: 'Bad credentials'
})
})
The module exposes a Vue composable to know if a user is authenticated in your application:
<script setup>
const { loggedIn, session, user, clear, fetch } = useUserSession()
</script>
Let's update our app.vue
to add a login form if the user is a guest or display his/her name if logged-in:
<script setup>
const { loggedIn, user, fetch, clear } = useUserSession()
const credentials = reactive({
email: '',
password: '',
})
async function login() {
$fetch('/api/login', {
method: 'POST',
body: credentials
})
.then(fetch)
.catch(() => alert('Bad credentials'))
}
</script>
<template>
<div v-if="loggedIn">
<h1>Welcome {{ user.name }}</h1>
<button @click="clear">Logout</button>
</div>
<form @submit.prevent="login" v-else>
<input v-model="credentials.email" type="email" placeholder="Email" />
<input v-model="credentials.password" type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</template>
This is what you should have in your browser when opening http://localhost:3000:
Next Steps
I won't go in this article about adding a database, but as you can guess, you don't want to hardcode the credentials but use a database for users to signup and login to your application.
I created two open source examples using a SQLite database with Drizzle, using CloudFlare D1 or Turso in production:
I hope this article thought you something, feel free to reach to me on Twitter / X for more Nuxt tips and announcements.