Use Amplify categories APIs from Nuxt 3
If you have not already done so, please read the introduction documentation, Use Amplify Categories APIs in Server-Side Rendering, to learn about how to use Amplify categories' APIs in server-side rendering.
This documentation provides a getting started guide to using the generic runWithAmplifyServerContext adapter (exported from aws-amplify/adapter-core) to enable Amplify in a Nuxt 3 project. The examples in this documentation may not present best practices for your Nuxt project. You are welcome to provide suggestions and contributions to improve this documentation or to create a Nuxt adapter package for Amplify and let others use it.
Start using Amplify in your Nuxt 3 project
You can install relevant Amplify libraries by following the manual installation guide.
Set up the AmplifyAPIs plugin
Nuxt 3 offers universal rendering by default, where your data fetching logic may be executed in both client and server runtimes. Amplify offers APIs that are capable of running within a server context to support use cases such as server-side rendering (SSR) and static site generation (SSG), though Amplify's client-side APIs and server-side APIs are slightly different. You can set up an AmplifyAPIs plugin to make your data fetching logic run smoothly across the client and server.
- If you haven’t already done so, create a
pluginsdirectory under the root of your Nuxt project - Create two files
01.amplifyApis.client.tsand01.amplifyApis.server.tsunder thepluginsdirectory
In these files, you will register both client-specific and server-specific Amplify APIs that you will use in your Nuxt project as a plugin. You can then access these APIs via the useNuxtApp composable.
Implement 01.amplifyApis.client.ts
Example implementation:
import type { Schema } from '~/amplify/data/resource';import { Amplify } from 'aws-amplify';import { fetchAuthSession, fetchUserAttributes, signIn, signOut} from 'aws-amplify/auth';import { generateClient } from 'aws-amplify/api';import { list } from 'aws-amplify/storage';
import outputs from '../amplify_outputs.json';
const client = generateClient<Schema>();
export default defineNuxtPlugin({ name: 'AmplifyAPIs', enforce: 'pre',
setup() { // This configures Amplify on the client side of your Nuxt app Amplify.configure(outputs, { ssr: true });
return { provide: { // You can add the Amplify APIs that you will use on the client side // of your Nuxt app here. // // You can call the API by via the composable `useNuxtApp()`. For example: // `useNuxtApp().$Amplify.Auth.fetchAuthSession()` Amplify: { Auth: { fetchAuthSession, fetchUserAttributes, signIn, signOut }, Storage: { list }, GraphQL: { client } } } }; }});Implement 01.amplifyApis.server.ts
Example implementation:
import type { FetchAuthSessionOptions } from 'aws-amplify/auth';import type { ListPaginateWithPathInput } from 'aws-amplify/storage';import type { CookieRef } from 'nuxt/app';import type { Schema } from '~/amplify/data/resource';
import { createAWSCredentialsAndIdentityIdProvider, createKeyValueStorageFromCookieStorageAdapter, createUserPoolsTokenProvider, runWithAmplifyServerContext} from 'aws-amplify/adapter-core';import { generateClient } from 'aws-amplify/api/server';import { fetchAuthSession, fetchUserAttributes, getCurrentUser} from 'aws-amplify/auth/server';import { list } from 'aws-amplify/storage/server';import { parseAmplifyConfig } from 'aws-amplify/utils';
import outputs from '../amplify_outputs.json';
// parse the content of `amplify_outputs.json` into the shape of ResourceConfigconst amplifyConfig = parseAmplifyConfig(outputs);
// create the Amplify used token cookies names arrayconst userPoolClientId = amplifyConfig.Auth!.Cognito.userPoolClientId;const lastAuthUserCookieName = `CognitoIdentityServiceProvider.${userPoolClientId}.LastAuthUser`;
// create a GraphQL client that can be used in a server contextconst gqlServerClient = generateClient<Schema>({ config: amplifyConfig });
// extract the model operation function types for creating wrapper function latertype RemoveFirstParam<Params extends any[]> = Params extends [infer _, ...infer Rest] ? Rest : never; type TodoListInput = RemoveFirstParam<Parameters<typeof gqlServerClient.models.Todo.list>>;type TodoCreateInput = RemoveFirstParam<Parameters<typeof gqlServerClient.models.Todo.create>>;type TodoUpdateInput = RemoveFirstParam<Parameters<typeof gqlServerClient.models.Todo.update>>;
const getAmplifyAuthKeys = (lastAuthUser: string) => ['idToken', 'accessToken', 'refreshToken', 'clockDrift'] .map( (key) => `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.${key}` ) .concat(lastAuthUserCookieName);
// define the pluginexport default defineNuxtPlugin({ name: 'AmplifyAPIs', enforce: 'pre', setup() { // The Nuxt composable `useCookie` is capable of sending cookies to the // client via the `SetCookie` header. If the `expires` option is left empty, // it sets a cookie as a session cookie. If you need to persist the cookie // on the client side after your end user closes your Web app, you need to // specify an `expires` value. // // We use 30 days here as an example (the default Cognito refreshToken // expiration time). const expires = new Date(); expires.setDate(expires.getDate() + 30);
// Get the last auth user cookie value // // We use `sameSite: 'lax'` in this example, which allows the cookie to be // sent to your Nuxt server when your end user gets redirected to your Web // app from a different domain. You should choose an appropriate value for // your own use cases. const lastAuthUserCookie = useCookie(lastAuthUserCookieName, { sameSite: 'lax', expires, secure: true });
// Get all Amplify auth token cookie names const authKeys = lastAuthUserCookie.value ? getAmplifyAuthKeys(lastAuthUserCookie.value) : [];
// Create a key-value map of cookie name => cookie ref // // Using the composable `useCookie` here in the plugin setup prevents // cross-request pollution. const amplifyCookies = authKeys .map((name) => ({ name, cookieRef: useCookie(name, { sameSite: 'lax', expires, secure: true }) })) .reduce<Record<string, CookieRef<string | null | undefined>>>( (result, current) => ({ ...result, [current.name]: current.cookieRef }), {} );
// Create a key value storage based on the cookies // // This key value storage is responsible for providing Amplify Auth tokens to // the APIs that you are calling. // // If you implement the `set` method, when Amplify needed to refresh the Auth // tokens on the server side, the new tokens would be sent back to the client // side via `SetCookie` header in the response. Otherwise the refresh tokens // would not be propagate to the client side, and Amplify would refresh // the tokens when needed on the client side. // // In addition, if you decide not to implement the `set` method, you don't // need to pass any `CookieOptions` to the `useCookie` composable. const keyValueStorage = createKeyValueStorageFromCookieStorageAdapter({ get(name) { const cookieRef = amplifyCookies[name];
if (cookieRef && cookieRef.value) { return { name, value: cookieRef.value }; }
return undefined; }, getAll() { return Object.entries(amplifyCookies).map(([name, cookieRef]) => { return { name, value: cookieRef.value ?? undefined }; }); }, set(name, value) { const cookieRef = amplifyCookies[name]; if (cookieRef) { cookieRef.value = value; } }, delete(name) { const cookieRef = amplifyCookies[name];
if (cookieRef) { cookieRef.value = null; } } });
// Create a token provider const tokenProvider = createUserPoolsTokenProvider( amplifyConfig.Auth!, keyValueStorage );
// Create a credentials provider const credentialsProvider = createAWSCredentialsAndIdentityIdProvider( amplifyConfig.Auth!, keyValueStorage );
// Create the libraryOptions object const libraryOptions = { Auth: { tokenProvider, credentialsProvider } };
return { provide: { // You can add the Amplify APIs that you will use on the server side of // your Nuxt app here. You must only use the APIs exported from the // `aws-amplify/<category>/server` subpaths. // // You can call the API by via the composable `useNuxtApp()`. For example: // `useNuxtApp().$Amplify.Auth.fetchAuthSession()` // // Recall that Amplify server APIs are required to be called in a isolated // server context that is created by the `runWithAmplifyServerContext` // function. Amplify: { Auth: { fetchAuthSession: (options: FetchAuthSessionOptions) => runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => fetchAuthSession(contextSpec, options) ), fetchUserAttributes: () => runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => fetchUserAttributes(contextSpec) ), getCurrentUser: () => runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => getCurrentUser(contextSpec) ) }, Storage: { list: (input: ListPaginateWithPathInput) => runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => list(contextSpec, input) ) }, GraphQL: { client: { models: { Todo: { list(...input: TodoListInput) { return runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => gqlServerClient.models.Todo.list(contextSpec, ...input) ) }, create(...input: TodoCreateInput) { return runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => gqlServerClient.models.Todo.create(contextSpec, ...input) ) }, update(...input: TodoUpdateInput) { return runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => gqlServerClient.models.Todo.update(contextSpec, ...input) ) } } } } } } } }; }});Usage example
Using the Storage list API in pages/storage-list.vue:
// `useAsyncData` and `useNuxtApp` are Nuxt composables// `$Amplify` is generated by Nuxt according to the `provide` key in the plugins// we've added above<script setup lang="ts">const { data, error } = useAsyncData(async () => { const listResult = await useNuxtApp().$Amplify.Storage.list({ path: 'album' }); return listResult.items;});</script>
<template> <h3>Files under path: album</h3> <pre>{{ data }}</pre></template>Using the GraphQL API in pages/todos-list.vue:
<script setup lang="ts">const { data, error } = useAsyncData(async () => { const { data } = await useNuxtApp().$Amplify.GraphQL.client.models.Todo.list(); return data;});</script>
<template> <h3>Todos</h3> <pre>{{ data }}</pre></template>The above two pages can be rendered on both the client and server by default. useNuxtApp().$Amplify will pick up the correct implementation of 01.amplifyApis.client.ts and 01.amplifyApis.server.ts to use, depending on the runtime.
Set up Auth middleware to protect your routes
The auth middleware will use the plugin configured in the previous step as a dependency; therefore you can add the auth middleware via another plugin that will be loaded after the previous one.
- Create a
02.authRedirect.tsfile under plugins directory
Implement 02.authRedirect.ts
Example implementation:
import { Amplify } from 'aws-amplify';
import outputs from '~/amplify_outputs.json';
// Amplify.configure() only needs to be called on the client sideif (process.client) { Amplify.configure(outputs, { ssr: true });}
export default defineNuxtPlugin({ name: 'AmplifyAuthRedirect', enforce: 'pre', setup() { addRouteMiddleware( 'AmplifyAuthMiddleware', defineNuxtRouteMiddleware(async (to) => { try { const session = await useNuxtApp().$Amplify.Auth.fetchAuthSession();
// If the request is not associated with a valid user session // redirect to the `/sign-in` route. // You can also add route match rules against `to.path` if (session.tokens === undefined && to.path !== '/sign-in') { return navigateTo('/sign-in'); }
if (session.tokens !== undefined && to.path === '/sign-in') { return navigateTo('/'); } } catch (e) { if (to.path !== '/sign-in') { return navigateTo('/sign-in'); } } }), { global: true } ); }});Set Up Amplify for API Route Use Cases
Following the specification of Nuxt, your API route handlers will live under ~/server, which is a separate environment from other parts of your Nuxt app; hence, the plugins created in the previous sections are not usable here, and extra work is required.
Set up Amplify server context utility
- If you haven’t already done so, create a
utilsdirectory under the server directory of your Nuxt project - Create an
amplifyUtils.tsfile under theutilsdirectory
In this file, you will create a helper function to call Amplify APIs that are capable of running on the server side with context isolation.
Example implementation:
import type { H3Event, EventHandlerRequest } from 'h3';
import { AmplifyServer, CookieStorage, createAWSCredentialsAndIdentityIdProvider, createKeyValueStorageFromCookieStorageAdapter, createUserPoolsTokenProvider, runWithAmplifyServerContext,} from 'aws-amplify/adapter-core';import { parseAmplifyConfig } from 'aws-amplify/utils';
import outputs from '~/amplify_outputs.json';
const amplifyConfig = parseAmplifyConfig(outputs);
const createCookieStorageAdapter = ( event: H3Event<EventHandlerRequest>): CookieStorage.Adapter => { // `parseCookies`, `setCookie` and `deleteCookie` are Nuxt provided functions const readOnlyCookies = parseCookies(event);
return { get(name) { if (readOnlyCookies[name]) { return { name, value: readOnlyCookies[name] }; } }, set(name, value, options) { setCookie(event, name, value, options); }, delete(name) { deleteCookie(event, name); }, getAll() { return Object.entries(readOnlyCookies).map(([name, value]) => { return { name, value }; }); } };};
const createLibraryOptions = ( event: H3Event<EventHandlerRequest>) => { const cookieStorage = createCookieStorageAdapter(event); const keyValueStorage = createKeyValueStorageFromCookieStorageAdapter(cookieStorage); const tokenProvider = createUserPoolsTokenProvider( amplifyConfig.Auth!, keyValueStorage ); const credentialsProvider = createAWSCredentialsAndIdentityIdProvider( amplifyConfig.Auth!, keyValueStorage );
return { Auth: { tokenProvider, credentialsProvider } };};
export const runAmplifyApi = <Result>( // we need the event object to create a context accordingly event: H3Event<EventHandlerRequest>, operation: ( contextSpec: AmplifyServer.ContextSpec ) => Result | Promise<Result>) => { return runWithAmplifyServerContext<Result>( amplifyConfig, createLibraryOptions(event), operation );};You can then use runAmplifyApi function to call Amplify APIs in an isolated server context.
Usage example
Take implementing an API route GET /api/current-user , in server/api/current-user.ts:
import { getCurrentUser } from 'aws-amplify/auth/server';
import { runAmplifyApi } from '~/server/utils/amplifyUtils';
export default defineEventHandler(async (event) => { const user = await runAmplifyApi(event, (contextSpec) => getCurrentUser(contextSpec) );
return user;});Then you can fetch data from this route, for example, fetch('http://localhost:3000/api/current-user').
Set up server middleware to protect your API routes
Similar to API routes, the previously added auth middleware are not usable under /server, hence extra work is required to set up a auth middleware to protect your routes.
- If you haven’t already done so, create a
middlewaredirectory under theserverdirectory of your Nuxt project - Create an
amplifyAuthMiddleware.tsfile under themiddlewaredirectory
This middleware will be executed before a request reach your API route.
Example implementation:
import { fetchAuthSession } from 'aws-amplify/auth/server';
export default defineEventHandler(async (event) => { if (event.path.startsWith('/api/')) { try { const session = await runAmplifyApi(event, (contextSpec) => fetchAuthSession(contextSpec) );
// You can add extra logic to match the requested routes to apply // the auth protection if (session.tokens === undefined) { setResponseStatus(event, 403); return { error: 'Access denied!' }; } } catch (error) { return { error: 'Access denied!' }; } }});With this middleware, when executing fetch('http://localhost:3000/api/current-user') without signing in a user on the client side, the fetch will receive a 403 error, and the request won’t reach route /api/current-user.