Page updated Mar 6, 2024

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.

NOTE: This guide assumes that you have a deep knowledge of Nuxt 3.

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.

  1. If you haven’t already done so, create a plugins directory under the root of your Nuxt project
  2. Create two files 01.amplify-apis.client.ts and 01.amplify-apis.server.ts under the plugins directory

NOTE: the leading number in the files name indicate the plugin loading order, details see https://nuxt.com/docs/guide/directory-structure/plugins#registration-order. The .client and .server indicate the runtime that the logic contained in the file will run on, client or server. For details see: https://nuxt.com/docs/guide/directory-structure/plugins

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 more
Expand to view the example implementation
1import { Amplify } from 'aws-amplify';
2import {
3 fetchAuthSession,
4 fetchUserAttributes,
5 signIn,
6 signOut
7} from 'aws-amplify/auth';
8import { list } from 'aws-amplify/storage';
9import { generateClient } from 'aws-amplify/api';
10import config from '../amplifyconfiguration.json';
11
12const client = generateClient();
13
14export default defineNuxtPlugin({
15 name: 'AmplifyAPIs',
16 enforce: 'pre',
17
18 setup() {
19 // This configures Amplify on the client side of your Nuxt app
20 Amplify.configure(config, { ssr: true });
21
22 return {
23 provide: {
24 // You can add the Amplify APIs that you will use on the client side
25 // of your Nuxt app here.
26 //
27 // You can call the API by via the composable `useNuxtApp()`. For example:
28 // `useNuxtApp().$Amplify.Auth.fetchAuthSession()`
29 Amplify: {
30 Auth: {
31 fetchAuthSession,
32 fetchUserAttributes,
33 signIn,
34 signOut
35 },
36 Storage: {
37 list
38 },
39 GraphQL: {
40 client
41 }
42 }
43 }
44 };
45 }
46});

Make sure you call Amplify.configure as early as possible in your application’s life-cycle. A missing configuration or NoCredentials error is thrown if Amplify.configure has not been called before other Amplify JavaScript APIs. Review the Library Not Configured Troubleshooting guide for possible causes of this issue.

Implement 01.amplify-apis.server.ts

Example implementation:

Learn more
Expand to view the example implementation
1import type { CookieRef } from 'nuxt/app';
2import {
3 createKeyValueStorageFromCookieStorageAdapter,
4 createUserPoolsTokenProvider,
5 createAWSCredentialsAndIdentityIdProvider,
6 runWithAmplifyServerContext
7} from 'aws-amplify/adapter-core';
8import { parseAmplifyConfig } from 'aws-amplify/utils';
9import {
10 fetchAuthSession,
11 fetchUserAttributes,
12 getCurrentUser
13} from 'aws-amplify/auth/server';
14import { list } from 'aws-amplify/storage/server';
15import { generateClient } from 'aws-amplify/api/server';
16import type { ListPaginateInput } from 'aws-amplify/storage';
17import type {
18 LibraryOptions,
19 FetchAuthSessionOptions
20} from '@aws-amplify/core';
21import type {
22 GraphQLOptionsV6,
23 GraphQLResponseV6
24} from '@aws-amplify/api-graphql';
25
26import config from '../amplifyconfiguration.json';
27
28// parse the content of `amplifyconfiguration.json` into the shape of ResourceConfig
29const amplifyConfig = parseAmplifyConfig(config);
30
31// create the Amplify used token cookies names array
32const userPoolClientId = amplifyConfig.Auth!.Cognito.userPoolClientId;
33const lastAuthUserCookieName = `CognitoIdentityServiceProvider.${userPoolClientId}.LastAuthUser`;
34
35// create a GraphQL client that can be used in a server context
36const gqlServerClient = generateClient({ config: amplifyConfig });
37
38const getAmplifyAuthKeys = (lastAuthUser: string) =>
39 ['idToken', 'accessToken', 'refreshToken', 'clockDrift']
40 .map(
41 (key) =>
42 `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.${key}`
43 )
44 .concat(lastAuthUserCookieName);
45
46// define the plugin
47export default defineNuxtPlugin({
48 name: 'AmplifyAPIs',
49 enforce: 'pre',
50 setup() {
51 // The Nuxt composable `useCookie` is capable of sending cookies to the
52 // client via the `SetCookie` header. If the `expires` option is left empty,
53 // it sets a cookie as a session cookie. If you need to persist the cookie
54 // on the client side after your end user closes your Web app, you need to
55 // specify an `expires` value.
56 //
57 // We use 30 days here as an example (the default Cognito refreshToken
58 // expiration time).
59 const expires = new Date();
60 expires.setDate(expires.getDate() + 30);
61
62 // Get the last auth user cookie value
63 //
64 // We use `sameSite: 'lax'` in this example, which allows the cookie to be
65 // sent to your Nuxt server when your end user gets redirected to your Web
66 // app from a different domain. You should choose an appropriate value for
67 // your own use cases.
68 const lastAuthUserCookie = useCookie(lastAuthUserCookieName, {
69 sameSite: 'lax',
70 expires,
71 secure: true
72 });
73
74 // Get all Amplify auth token cookie names
75 const authKeys = lastAuthUserCookie.value
76 ? getAmplifyAuthKeys(lastAuthUserCookie.value)
77 : [];
78
79 // Create a key-value map of cookie name => cookie ref
80 //
81 // Using the composable `useCookie` here in the plugin setup prevents
82 // cross-request pollution.
83 const amplifyCookies = authKeys
84 .map((name) => ({
85 name,
86 cookieRef: useCookie(name, { sameSite: 'lax', expires, secure: true })
87 }))
88 .reduce<Record<string, CookieRef<string | null | undefined>>>(
89 (result, current) => ({
90 ...result,
91 [current.name]: current.cookieRef
92 }),
93 {}
94 );
95
96 // Create a key value storage based on the cookies
97 //
98 // This key value storage is responsible for providing Amplify Auth tokens to
99 // the APIs that you are calling.
100 //
101 // If you implement the `set` method, when Amplify needed to refresh the Auth
102 // tokens on the server side, the new tokens would be sent back to the client
103 // side via `SetCookie` header in the response. Otherwise the refresh tokens
104 // would not be propagate to the client side, and Amplify would refresh
105 // the tokens when needed on the client side.
106 //
107 // In addition, if you decide not to implement the `set` method, you don't
108 // need to pass any `CookieOptions` to the `useCookie` composable.
109 const keyValueStorage = createKeyValueStorageFromCookieStorageAdapter({
110 get(name) {
111 const cookieRef = amplifyCookies[name];
112
113 if (cookieRef && cookieRef.value) {
114 return { name, value: cookieRef.value };
115 }
116
117 return undefined;
118 },
119 getAll() {
120 return Object.entries(amplifyCookies).map(([name, cookieRef]) => {
121 return { name, value: cookieRef.value ?? undefined };
122 });
123 },
124 set(name, value) {
125 const cookieRef = amplifyCookies[name];
126 if (cookieRef) {
127 cookieRef.value = value;
128 }
129 },
130 delete(name) {
131 const cookieRef = amplifyCookies[name];
132
133 if (cookieRef) {
134 cookieRef.value = null;
135 }
136 }
137 });
138
139 // Create a token provider
140 const tokenProvider = createUserPoolsTokenProvider(
141 amplifyConfig.Auth!,
142 keyValueStorage
143 );
144
145 // Create a credentials provider
146 const credentialsProvider = createAWSCredentialsAndIdentityIdProvider(
147 amplifyConfig.Auth!,
148 keyValueStorage
149 );
150
151 // Create the libraryOptions object
152 const libraryOptions: LibraryOptions = {
153 Auth: {
154 tokenProvider,
155 credentialsProvider
156 }
157 };
158
159 return {
160 provide: {
161 // You can add the Amplify APIs that you will use on the server side of
162 // your Nuxt app here. You must only use the APIs exported from the
163 // `aws-amplify/<category>/server` subpaths.
164 //
165 // You can call the API by via the composable `useNuxtApp()`. For example:
166 // `useNuxtApp().$Amplify.Auth.fetchAuthSession()`
167 //
168 // Recall that Amplify server APIs are required to be called in a isolated
169 // server context that is created by the `runWithAmplifyServerContext`
170 // function.
171 Amplify: {
172 Auth: {
173 fetchAuthSession: (options: FetchAuthSessionOptions) =>
174 runWithAmplifyServerContext(
175 amplifyConfig,
176 libraryOptions,
177 (contextSpec) => fetchAuthSession(contextSpec, options)
178 ),
179 fetchUserAttributes: () =>
180 runWithAmplifyServerContext(
181 amplifyConfig,
182 libraryOptions,
183 (contextSpec) => fetchUserAttributes(contextSpec)
184 ),
185 getCurrentUser: () =>
186 runWithAmplifyServerContext(
187 amplifyConfig,
188 libraryOptions,
189 (contextSpec) => getCurrentUser(contextSpec)
190 )
191 },
192 Storage: {
193 list: (input: ListPaginateInput) =>
194 runWithAmplifyServerContext(
195 amplifyConfig,
196 libraryOptions,
197 (contextSpec) => list(contextSpec, input)
198 )
199 },
200 GraphQL: {
201 client: {
202 // Follow this typing to ensure the`graphql` API return type can
203 // be inferred correctly according to your queries and mutations
204 graphql: <
205 FALLBACK_TYPES = unknown,
206 TYPED_GQL_STRING extends string = string
207 >(
208 options: GraphQLOptionsV6<FALLBACK_TYPES, TYPED_GQL_STRING>,
209 additionalHeaders?: Record<string, string>
210 ) =>
211 runWithAmplifyServerContext<
212 GraphQLResponseV6<FALLBACK_TYPES, TYPED_GQL_STRING>
213 >(amplifyConfig, libraryOptions, (contextSpec) =>
214 gqlServerClient.graphql(
215 contextSpec,
216 options,
217 additionalHeaders
218 )
219 )
220 }
221 }
222 }
223 }
224 };
225 }
226});

Usage Example

Using the Storage list API in ~/pages/storage-list.vue:

1// `useAsyncData` and `useNuxtApp` are Nuxt composables
2// `$Amplify` is generated by Nuxt according to the `provide` key in the plugins
3// we've added above
4<script setup lang="ts">
5const { data, error } = useAsyncData(async () => {
6 const listResult = await useNuxtApp().$Amplify.Storage.list({
7 options: {accessLevel: 'guest'}
8 });
9 return listResult.items;
10});
11</script>
12
13<template>
14 <h3>Files with access level: guest</h3>
15 <pre>{{ data }}</pre>
16</template>

Using the GraphQL API in ~/pages/todos-list.vue:

1<script setup lang="ts">
2// Amplify codegen generated code after you run `amplify push` or `amplify pull`
3import { listTodos } from '~/graphql/queries';
4
5const { data, error } = useAsyncData(async () => {
6 const result = await useNuxtApp().$Amplify.GraphQL.client.graphql({
7 query: listTodos,
8 });
9 return result.data.listTodos;
10});
11</script>
12
13<template>
14 <h3>Todos</h3>
15 <pre>{{ data }}</pre>
16</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.

Only a subset of Amplify APIs are usable on the server side, and as the libraries evolve, amplify-apis.client and amplify-apis.server may diverge further. You can guard your API calls to ensure an API is available in the context where you use it. E.g., you can use if (process.client) to ensure that a client-only API isn't executed on the server.

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.

  1. Create a 02.auth-redirect.ts file under plugins directory

NOTE: This file will run on both client and server, details see: https://nuxt.com/docs/guide/directory-structure/middleware#when-middleware-runs. The 02 name prefix ensures this plugin loads after the previous so useNuxtApp().$Amplify becomes available.

Implement 02.auth-redirect.ts

Example implementation:

Learn more
Expand to view the example implementation
1import { Amplify } from 'aws-amplify';
2import config from '~/amplifyconfiguration.json';
3
4// Amplify.configure() only needs to be called on the client side
5if (process.client) {
6 Amplify.configure(config, { ssr: true });
7}
8
9export default defineNuxtPlugin({
10 name: 'AmplifyAuthRedirect',
11 enforce: 'pre',
12 setup() {
13 addRouteMiddleware(
14 'AmplifyAuthMiddleware',
15 defineNuxtRouteMiddleware(async (to) => {
16 try {
17 const session = await useNuxtApp().$Amplify.Auth.fetchAuthSession();
18
19 // If the request is not associated with a valid user session
20 // redirect to the `/sign-in` route.
21 // You can also add route match rules against `to.path`
22 if (session.tokens === undefined && to.path !== '/sign-in') {
23 return navigateTo('/sign-in');
24 }
25
26 if (session.tokens !== undefined && to.path === '/sign-in') {
27 return navigateTo('/');
28 }
29 } catch (e) {
30 if (to.path !== '/sign-in') {
31 return navigateTo('/sign-in');
32 }
33 }
34 }),
35 { global: true }
36 );
37 }
38});

Make sure you call Amplify.configure as early as possible in your application’s life-cycle. A missing configuration or NoCredentials error is thrown if Amplify.configure has not been called before other Amplify JavaScript APIs. Review the Library Not Configured Troubleshooting guide for possible causes of this issue.

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

  1. If you haven’t already done so, create a utils directory under the server directory of your Nuxt project
  2. Create an amplifyUtils.ts file under the utils 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 more
Expand to view the example implementation
1import type { H3Event, EventHandlerRequest } from 'h3';
2import {
3 createKeyValueStorageFromCookieStorageAdapter,
4 createUserPoolsTokenProvider,
5 createAWSCredentialsAndIdentityIdProvider,
6 runWithAmplifyServerContext,
7 AmplifyServer,
8 CookieStorage
9} from 'aws-amplify/adapter-core';
10import { parseAmplifyConfig } from 'aws-amplify/utils';
11
12import type { LibraryOptions } from '@aws-amplify/core';
13import config from '~/amplifyconfiguration.json';
14
15const amplifyConfig = parseAmplifyConfig(config);
16
17const createCookieStorageAdapter = (
18 event: H3Event<EventHandlerRequest>
19): CookieStorage.Adapter => {
20 // `parseCookies`, `setCookie` and `deleteCookie` are Nuxt provided functions
21 const readOnlyCookies = parseCookies(event);
22
23 return {
24 get(name) {
25 if (readOnlyCookies[name]) {
26 return { name, value: readOnlyCookies[name] };
27 }
28 },
29 set(name, value, options) {
30 setCookie(event, name, value, options);
31 },
32 delete(name) {
33 deleteCookie(event, name);
34 },
35 getAll() {
36 return Object.entries(readOnlyCookies).map(([name, value]) => {
37 return { name, value };
38 });
39 }
40 };
41};
42
43const createLibraryOptions = (
44 event: H3Event<EventHandlerRequest>
45): LibraryOptions => {
46 const cookieStorage = createCookieStorageAdapter(event);
47 const keyValueStorage =
48 createKeyValueStorageFromCookieStorageAdapter(cookieStorage);
49 const tokenProvider = createUserPoolsTokenProvider(
50 amplifyConfig.Auth!,
51 keyValueStorage
52 );
53 const credentialsProvider = createAWSCredentialsAndIdentityIdProvider(
54 amplifyConfig.Auth!,
55 keyValueStorage
56 );
57
58 return {
59 Auth: {
60 tokenProvider,
61 credentialsProvider
62 }
63 };
64};
65
66export const runAmplifyApi = <Result>(
67 // we need the event object to create a context accordingly
68 event: H3Event<EventHandlerRequest>,
69 operation: (
70 contextSpec: AmplifyServer.ContextSpec
71 ) => Result | Promise<Result>
72) => {
73 return runWithAmplifyServerContext<Result>(
74 amplifyConfig,
75 createLibraryOptions(event),
76 operation
77 );
78};

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:

1import { getCurrentUser } from 'aws-amplify/auth/server';
2import { runAmplifyApi } from '~/server/utils/amplifyUtils';
3
4export default defineEventHandler(async (event) => {
5 const user = await runAmplifyApi(event, (contextSpec) =>
6 getCurrentUser(contextSpec)
7 );
8
9 return user;
10});

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.

  1. If you haven’t already done so, create a middleware directory under the server directory of your Nuxt project
  2. Create an amplifyAuthMiddleware.ts file under the middleware directory

This middleware will be executed before a request reach your API route.

Example implementation:

1import { fetchAuthSession } from 'aws-amplify/auth/server';
2
3export default defineEventHandler(async (event) => {
4 if (event.path.startsWith('/api/')) {
5 try {
6 const session = await runAmplifyApi(event, (contextSpec) =>
7 fetchAuthSession(contextSpec)
8 );
9
10 // You can add extra logic to match the requested routes to apply
11 // the auth protection
12 if (session.tokens === undefined) {
13 setResponseStatus(event, 403);
14 return {
15 error: 'Access denied!'
16 };
17 }
18 } catch (error) {
19 return {
20 error: 'Access denied!'
21 };
22 }
23 }
24});

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.