Skip to content

Nuxt 3 API Auth Technique

Authenticating Requests to Nuxt 3 Endpoints

When converting a Nuxt 2 app with an Express.js API to Nuxt 3, I quickly encountered some new challenges. It was tricky to convert the API to the Nitro File-Based Endpoint API of the Nuxt 3 framework, and I found little content about how to implement auth with the API server handlers. I wanted to implement cookie-based authentication with my API server handlers and have SSR pages, which is convenient with Nuxt 3, despite the tediousness of the file-based API.

TLDR: Use a wrapper function on your event handlers.
💡 This Nuxt project uses the following Package.json dependencies:
"dependencies": {
    "@supabase/supabase-js": "^2.4.0"
  "devDependencies": {
    "nuxt": "^3.0.0",
    "@nuxtjs/supabase": "^0.3.0"
  }Code language: JSON / JSON with Comments (json)

Overview of Implementation

I have an implementation where the Nuxt App client will send an auth cookie with API requests. Then, Nuxt server middleware handles validating the auth cookie and sets a user object on the event.context if the cookie is valid. The API’s eventHandlers then check for the user and respond with a 401 if it is not present. I use cookies, but the outline is similar for use with tokens.

Nuxt 3 Details – Client Side

On the front end, I am using @supabase/supabase-js. All you need to know about Supabase in this context is that it has an OAuth client. The Supabase client will grab auth tokens from the redirect query string, so I subscribe to the Supabase client’s onAuthStateChange event to set cookies corresponding to the client’s auth tokens.

// This code best lives in Nuxt plugin where you can expose the Supabase client to the app 
// or just use @nuxtjs/supabase which does that for you: <>
const supabase = createClient(url, key);
supabase.auth.onAuthStateChange((event, session) => {
      if (event === "SIGNED_OUT" || event === "USER_DELETED") {
        // delete cookies on sign out
        const expires = new Date(0).toUTCString();
        document.cookie = <code>sb-access-token=; path=/; expires=${expires}; SameSite=Lax; secure</code>;
        document.cookie = <code>sb-refresh-token=; path=/; expires=${expires}; SameSite=Lax; secure</code>;
      } else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
        const maxAge = 100 * 365 * 24 * 60 * 60; // 1 year, (but the Supabase token probably expires before then)
        document.cookie = <code>sb-access-token=${session?.access_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure</code>;
        document.cookie = <code>sb-refresh-token=${session?.refresh_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure</code>;
Code language: TypeScript (typescript)

I have a sign-in button that invokes the signInWithOAuth function of the Supabase client.

// index.vue
      provider: "google",
      options: { redirectTo: redirectUrl, scopes: this.scopes.join(" ") },
    });Code language: TypeScript (typescript)

Of course, you can use the usual pattern of getting a cookie from the server via a Nitro API endpoint’s EventHandler for such a purpose, but the scenario is the same at this point: the client has a cookie that can be sent to the server.

Now that I have a cookie on the front end, I need to use it to authenticate API requests, specifically SSR ones.  The SSR-friendly useFetch of Nuxt 3 has a headers option where we must ensure cookies are attached for the SSR requests. I have written another simple composable called useCookieHeader that attaches cookies (which includes the Supabase auth ones) to the headers option of the request.

// pages/index.vue
      <button @click="refresh"></button >
      <p>{{ data }}</p>

<script setup lang="ts">
// 'data' will be populated as part of SSR! So useFetch ran on both the client and server.
// This means that if you api endpoint (authTest in this case) is async and takes 3 seconds to respond, 
// the page wont load for 3 seconds (see useFetch versus useLazyFetch). Try it out!
const { data, refresh } = await useFetch("/api/authTest", useCookieHeader());
</script>Code language: TypeScript (typescript)
// composables/useCookieHeader.ts

export const useCookieHeader = () => {
  // useRequestHeaders is a built-in composable to access the incoming request headers
  const cookieRecord = useRequestHeaders(["cookie"]);
  // the cookie could be null so checking if cookieRecord.cookie is truthy and
  // if it is, add a new property cookie to the headers object and
  // assigns the value of cookieRecord.cookie (which will have the Supabase tokens) to it.
  const headers: HeadersInit = {
    ...(cookieRecord.cookie && { cookie: cookieRecord.cookie }),

  return { headers };
};Code language: JavaScript (javascript)

Nuxt 3 Details – Server Side

On the server side, I created a middleware eventHandler that adds a user to the H3Event (see Nuxt API Layer for info about its use of H3) object that is passed around between eventHandlers.

// server/middleware/addUser.ts

// using "@nuxtjs/supabase": "^0.3.0" 
import { serverSupabaseUser } from "#supabase/server";

export default defineEventHandler(async (event) => {
// the composable serverSupabaseUser() will use the cookies on the event and get the user!
// see
// note this is a slow way of checking auth (making supabase requests). 
const user = await serverSupabaseUser(event);
  if (user) {
    event.context.user = { user };
});Code language: TypeScript (typescript)

The addUser middleware will run before our API event handlers. When the middleware gets to our function defined for our endpoint that requires an authenticated user, we can check for the user on the H3Event. I have written a helper wrapper function defineRequireAuthEventHandler that mimics the behavior of H3’s defineEventHandler but checks for the user that the addUser would have added and responds with a 401 if the user is not present. If the user is authenticated, the wrapper function responds with your desired response.

// server/defineRequireAuthEventHandler .ts
import { EventHandler } from "h3";

export const defineRequireAuthEventHandler = <T>(
  handler: EventHandler<T>
): EventHandler<T> => {
  handler.__is_handler__ = true;

  return eventHandler((event) => {
    console.log(<code>Checking auth</code>);
    const user = event.context.user;
    if (!user) {
      console.log("no user authenticated");
      throw createError({ statusCode: 401, statusMessage: "Unauthenticated" });

    console.log("user authenticated");

    return handler(event);
};Code language: TypeScript (typescript)

Example Usage

// server/api/authTest.ts

import { H3Event } from "h3";
import { defineRequireAuthEventHandler } from "../defineRequireAuthEventHandler";

const whoAmI = (event: H3Event) => <code>Hello ${}.</code>;

export default defineRequireAuthEventHandler(whoAmI);Code language: TypeScript (typescript)

That’s All, Folks

That wraps up the basic auth structure I use for a small app without many API endpoints. Check out @nuxtjs/supabase if you are using Supabase.

Update: I have come across the library @sidebase/nuxt-auth. Overall @sidebase/nuxt-auth provides an implementation of OAuth based auth itself, so you don’t have to roll your own if you aren’t using providers like Firebase or SupaBase. This post should give you a good understanding of how that library intends you to do session access and route protection as it prescribes essentially the same technique this post explains. For example, in the server/middleware/addUser.ts example of this post, instead of using Supabase client to validate auth tokens or cookies on the event, one would use the sidebase client.

Want More?

Want to create an enterprise-level Vue-based website? Check out the Coalesce framework.

Leverage its auto-generation of views, view models, loaders, and role-based API Controllers, to get reliable typing and authorization for your data-intensive web service!

Does Your Organization Need a Custom Solution?

Let’s chat about how we can help you achieve excellence on your next project!