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.
Set Up the AmplifyAPIs Plugin
Nuxt 3 offers universal rendering by default, where your data fetching logic may be executed on both the client and server sides. 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 of Amplify 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
plugins
directory under the root of your Nuxt project - Create two files
01.amplify-apis.client.ts
and01.amplify-apis.server.ts
under theplugins
directory
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.amplify-apis.client.ts
Example implementation:
Learn moreExpand to view the example implementation
import { Amplify } from 'aws-amplify';import { fetchAuthSession, fetchUserAttributes, signIn, signOut} from 'aws-amplify/auth';import { list } from 'aws-amplify/storage';import { generateClient } from 'aws-amplify/api';import config from '../amplifyconfiguration.json';
const client = generateClient();
export default defineNuxtPlugin({ name: 'AmplifyAPIs', enforce: 'pre',
setup() { // This configures Amplify on the client side of your Nuxt app Amplify.configure(config, { 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.amplify-apis.server.ts
Example implementation:
Learn moreExpand to view the example implementation
import type { CookieRef } from 'nuxt/app';import { createKeyValueStorageFromCookieStorageAdapter, createUserPoolsTokenProvider, createAWSCredentialsAndIdentityIdProvider, runWithAmplifyServerContext} from 'aws-amplify/adapter-core';import { parseAmplifyConfig } from 'aws-amplify/utils';import { fetchAuthSession, fetchUserAttributes, getCurrentUser} from 'aws-amplify/auth/server';import { list } from 'aws-amplify/storage/server';import { generateClient } from 'aws-amplify/api/server';import type { ListPaginateInput } from 'aws-amplify/storage';import type { LibraryOptions, FetchAuthSessionOptions} from '@aws-amplify/core';import type { GraphQLOptionsV6, GraphQLResponseV6} from '@aws-amplify/api-graphql';
import config from '../amplifyconfiguration.json';
// parse the content of `amplifyconfiguration.json` into the shape of ResourceConfigconst amplifyConfig = parseAmplifyConfig(config);
// 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({ config: amplifyConfig });
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: 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: ListPaginateInput) => runWithAmplifyServerContext( amplifyConfig, libraryOptions, (contextSpec) => list(contextSpec, input) ) }, GraphQL: { client: { // Follow this typing to ensure the`graphql` API return type can // be inferred correctly according to your queries and mutations graphql: < FALLBACK_TYPES = unknown, TYPED_GQL_STRING extends string = string >( options: GraphQLOptionsV6<FALLBACK_TYPES, TYPED_GQL_STRING>, additionalHeaders?: Record<string, string> ) => runWithAmplifyServerContext< GraphQLResponseV6<FALLBACK_TYPES, TYPED_GQL_STRING> >(amplifyConfig, libraryOptions, (contextSpec) => gqlServerClient.graphql( contextSpec, options, additionalHeaders ) ) } } } } }; }});
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({ options: {accessLevel: 'guest'} }); return listResult.items;});</script>
<template> <h3>Files with access level: guest</h3> <pre>{{ data }}</pre></template>
Using the GraphQL API in ~/pages/todos-list.vue
:
<script setup lang="ts">// Amplify codegen generated code after you run `amplify push` or `amplify pull`import { listTodos } from '~/graphql/queries';
const { data, error } = useAsyncData(async () => { const result = await useNuxtApp().$Amplify.GraphQL.client.graphql({ query: listTodos, }); return result.data.listTodos;});</script>
<template> <h3>Todos</h3> <pre>{{ data }}</pre></template>
The above two pages can be rendered on both the client and server sides by default. useNuxtApp().$Amplify
will pick up the correct implementation of 01.amplify-apis.client.ts
and 01.amplify-apis.server.ts
to use, depending on the runtime.
Set Up Auth Middleware to Protect Your Routes
The auth middleware will use the plugin set up 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.auth-redirect.ts
file under plugins directory
Implement 02.auth-redirect.ts
Example implementation:
Learn moreExpand to view the example implementation
import { Amplify } from 'aws-amplify';import config from '~/amplifyconfiguration.json';
// Amplify.configure() only needs to be called on the client sideif (process.client) { Amplify.configure(config, { 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
utils
directory under the server directory of your Nuxt project - Create an
amplifyUtils.ts
file under theutils
directory
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:
Learn moreExpand to view the example implementation
import type { H3Event, EventHandlerRequest } from 'h3';import { createKeyValueStorageFromCookieStorageAdapter, createUserPoolsTokenProvider, createAWSCredentialsAndIdentityIdProvider, runWithAmplifyServerContext, AmplifyServer, CookieStorage} from 'aws-amplify/adapter-core';import { parseAmplifyConfig } from 'aws-amplify/utils';
import type { LibraryOptions } from '@aws-amplify/core';import config from '~/amplifyconfiguration.json';
const amplifyConfig = parseAmplifyConfig(config);
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>): LibraryOptions => { 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, e.g. 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
middleware
directory under theserver
directory of your Nuxt project - Create an
amplifyAuthMiddleware.ts
file under themiddleware
directory
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
.