Full Stack Development with Nuxt

Learn how to create a Nuxt 3 full stack project with authentication and working on edge platforms.
I am building NuxtHub to help your build & ship full-stack Nuxt applications on the Edge 🚀

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:

server/api/hello.ts
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):

nuxt-devtools-api-routes

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')
Read more about all the Nuxt server route features.

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:

server/api/login.post.ts
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' }
})
Make sure to install the 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:

nuxt-devtools-api-route-400

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:

Terminal
npx nuxi@latest module add auth-utils
This command will install 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.

.env
NUXT_SESSION_PASSWORD=password-with-at-least-32-characters
If no 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:

server/api/login.post.ts
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'
  })
})
Read more about the setUserSession util exposed by nuxt-auth-utils.

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:

app.vue
<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:

Nuxt Todos Edge

A todos application with user authentication, SSR and Cloudflare D1 or Turso.

Nuxt Guestbook

A guestbook where you can sign-in with GitHub and say something about Nuxt.

I hope this article thought you something, feel free to reach to me on Twitter / X for more Nuxt tips and announcements.