Nine Essential Tips of AWS Amplify for Boosting Development Productivity

Overview

AWS Amplify is a powerful set of tools and services for developing, hosting, and managing serverless applications. With the recent launch of Amplify Gen 212, the platform has evolved significantly to enhance the developer experience. In this guide, we'll explore nine essential tips that will help you maximize your productivity with AWS Amplify, covering everything from authentication and infrastructure management to AI integration and deployment.

Understanding Amplify Gen 2

Before diving into the tips, let's understand what makes Amplify Gen 2 special. It introduces a code-first developer experience that enables building fullstack applications using TypeScript. Key benefits include:

  • TypeScript-first backend development
  • Faster local development with cloud sandbox environments
  • Improved team workflows with fullstack Git branches
  • Unified management console
  • Enhanced integration with AWS CDK

Tip 1: Implementing Third-Party Authentication

AWS Amplify provides seamless integration with popular authentication providers like Google, Facebook, and Amazon. You can also leverage any service supporting industry-standard protocols like OpenID Connect (OIDC) or SAML. While the built-in Authenticator component doesn't directly support third-party provider customization, you can achieve this through Header and Footer customization.

 1<Authenticator
 2  components={{
 3    Header: SignInHeader,
 4    SignIn: {
 5      Header() {
 6        return (
 7          <div className="px-8 py-2">
 8            <Flex direction="column"
 9                  className="federated-sign-in-container">
10                  <Button
11                    onClick={async () => {
12                      await signInWithRedirect({
13                        provider: {
14                          custom: 'OIDC-Provider' // OIDC Provider name created in Cognito User Pool
15                        }
16                      });
17                    }}
18                    className="federated-sign-in-button"
19                    gap="1rem"
20                  >
21                    <svg
22                      xmlns="http://www.w3.org/2000/svg"
23                      fill="#000"
24                      version="1.1"
25                      viewBox="0 0 32 32"
26                      xmlSpace="preserve"
27                      className="amplify-icon federated-sign-in-icon"
28                    >
29                      <path
30                        d="M31 31.36H1v-.72h30v.72zm0-7H1A.36.36 0 01.64 24V1A.36.36 0 011 .64h30a.36.36 0 01.36.36v23a.36.36 0 01-.36.36zm-29.64-.72h29.28V1.36H1.36v22.28zm7.304-7.476c-.672 0-1.234-.128-1.687-.385s-.842-.6-1.169-1.029l.798-.644c.28.355.593.628.938.819.345.191.747.287 1.204.287.476 0 .847-.103 1.113-.308.266-.206.399-.495.399-.868 0-.28-.091-.52-.273-.721-.182-.201-.511-.338-.987-.414l-.574-.084a4.741 4.741 0 01-.924-.217c-.28-.098-.525-.229-.735-.392s-.374-.366-.49-.609a1.983 1.983 0 01-.175-.868c0-.354.065-.665.196-.931.13-.266.31-.488.539-.665s.501-.311.819-.399a3.769 3.769 0 011.022-.133c.588 0 1.08.103 1.477.308.396.206.744.49 1.043.854l-.742.672c-.159-.224-.392-.427-.7-.609-.308-.182-.695-.272-1.162-.272s-.819.1-1.057.3c-.238.201-.357.474-.357.819 0 .354.119.611.357.77.238.159.581.275 1.029.35l.56.084c.803.122 1.372.353 1.708.693.336.341.504.786.504 1.337 0 .7-.238 1.251-.714 1.652-.476.402-1.13.603-1.96.603zm6.733 0c-.672 0-1.234-.128-1.687-.385s-.842-.6-1.169-1.029l.798-.644c.28.355.593.628.938.819.345.191.747.287 1.204.287.476 0 .847-.103 1.113-.308.266-.206.399-.495.399-.868 0-.28-.091-.52-.273-.721-.182-.201-.511-.338-.987-.413l-.574-.084c-.336-.046-.644-.119-.924-.217s-.525-.229-.735-.392-.374-.366-.49-.609a1.983 1.983 0 01-.175-.868c0-.354.065-.665.196-.931.13-.266.31-.488.539-.665.229-.177.501-.311.819-.399a3.769 3.769 0 011.022-.133c.588 0 1.08.103 1.477.308.396.206.744.49 1.043.854l-.742.672c-.158-.224-.392-.427-.7-.609s-.695-.273-1.162-.273-.819.101-1.057.301c-.238.201-.357.474-.357.819 0 .354.119.611.357.77s.581.275 1.029.35l.56.084c.803.122 1.372.353 1.708.693.337.341.505.786.505 1.337 0 .7-.238 1.251-.715 1.652-.475.401-1.129.602-1.96.602zm7.378 0c-.485 0-.929-.089-1.33-.266s-.744-.432-1.028-.763a3.584 3.584 0 01-.665-1.19 4.778 4.778 0 01-.238-1.561c0-.569.079-1.087.238-1.554a3.56 3.56 0 01.665-1.197c.284-.332.627-.586 1.028-.763s.845-.266 1.33-.266.927.089 1.323.266.739.432 1.029.763c.289.331.513.73.672 1.197.158.467.238.985.238 1.554 0 .579-.08 1.099-.238 1.561a3.546 3.546 0 01-.672 1.19c-.29.331-.633.585-1.029.763a3.19 3.19 0 01-1.323.266zm0-.995c.606 0 1.102-.187 1.484-.56.383-.373.574-.942.574-1.708v-1.036c0-.765-.191-1.334-.574-1.708s-.878-.56-1.484-.56-1.102.187-1.483.56c-.383.374-.574.943-.574 1.708v1.036c0 .766.191 1.335.574 1.708.382.374.877.56 1.483.56z"></path>
31                      <path fill="none" d="M0 0H32V32H0z"></path>
32                    </svg>
33                    <span style={{color: "white !important"}}>Sign In with My OIDC Provider</span>
34                  </Button>
35                <Divider label="or" size="small"/>
36            </Flex>
37          </div>
38        );
39      }
40    }
41  }}
42  loginMechanisms={['email']}
43  signUpAttributes={['email']}
44  initialState="signIn"
45  hideSignUp={true}
46/>

Tip 2: Building Passwordless Authentication

Amazon Cognito now supports passwordless authentication, including sign-in with passkeys, email, and text messages. While the Authenticator component doesn't natively support these features, you can create a custom authentication experience using the Amplify JS library.

  1import { useState } from 'react';
  2import { useRouter } from 'next/navigation';
  3import { signIn, confirmSignIn, fetchUserAttributes } from 'aws-amplify/auth';
  4import { TextField, Button, CircularProgress, Alert } from '@mui/material';
  5
  6export default function Home() {
  7  const [email, setEmail] = useState('');
  8  const [code, setCode] = useState('');
  9  const [loading, setLoading] = useState(false);
 10  const [error, setError] = useState('');
 11  const [showConfirmation, setShowConfirmation] = useState(false);
 12  const router = useRouter();
 13
 14  const handleSignIn = async (e: React.FormEvent) => {
 15    e.preventDefault();
 16    setLoading(true);
 17    setError('');
 18    
 19    try {
 20      const { nextStep } = await signIn({
 21        username: email,
 22        options: {
 23          authFlowType: 'USER_AUTH',
 24          preferredChallenge: 'EMAIL_OTP',
 25        },
 26      });
 27      if (nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE' ||
 28        nextStep.signInStep === 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION'
 29      ) {
 30        setShowConfirmation(true);
 31      }
 32    } catch (err) {
 33      setError(err instanceof Error ? err.message : 'Sign in failed');
 34    } finally {
 35      setLoading(false);
 36    }
 37  };
 38
 39  const handleConfirmSignIn = async (e: React.FormEvent) => {
 40    e.preventDefault();
 41    setLoading(true);
 42    setError('');
 43
 44    try {
 45      const { nextStep: confirmSignInNextStep } = await confirmSignIn({ challengeResponse: code });
 46
 47      if (confirmSignInNextStep.signInStep === 'DONE') {
 48      const attributes = await fetchUserAttributes();
 49      if (attributes.email) {
 50        router.push('/home');
 51      }
 52      }
 53    } catch (err) {
 54      setError(err instanceof Error ? err.message : 'Confirmation failed');
 55    } finally {
 56      setLoading(false);
 57    }
 58  };
 59
 60  return (
 61    <div className="flex items-center justify-center min-h-screen">
 62      <div className="w-full max-w-md p-6">
 63        <div className="text-center mb-8">
 64          <h1 className="text-2xl font-bold mb-2">Sign in to My App</h1>
 65          <p className="text-gray-600">
 66            {showConfirmation ? 'Enter the code sent to your email' : 'Enter your email to receive a code'}
 67          </p>
 68        </div>
 69
 70        {error && (
 71          <Alert severity="error" className="mb-4">
 72            {error}
 73          </Alert>
 74        )}
 75
 76        {!showConfirmation ? (
 77          <form onSubmit={handleSignIn}>
 78            <TextField
 79              fullWidth
 80              label="Email"
 81              type="email"
 82              value={email}
 83              onChange={(e) => setEmail(e.target.value)}
 84              disabled={loading}
 85              required
 86              className="mb-4"
 87            />
 88            <Button
 89              fullWidth
 90              variant="contained"
 91              type="submit"
 92              disabled={loading}
 93              className="mt-2"
 94            >
 95              {loading ? <CircularProgress size={24} /> : 'Continue'}
 96            </Button>
 97          </form>
 98        ) : (
 99          <form onSubmit={handleConfirmSignIn}>
100            <TextField
101              fullWidth
102              label="Verification Code"
103              value={code}
104              onChange={(e) => setCode(e.target.value)}
105              disabled={loading}
106              required
107              className="mb-4"
108            />
109            <Button
110              fullWidth
111              variant="contained"
112              type="submit"
113              disabled={loading}
114              className="mt-2"
115            >
116              {loading ? <CircularProgress size={24} /> : 'Verify'}
117            </Button>
118          </form>
119        )}
120      </div>
121    </div>
122  );
123}

Tip 3: Managing Backend Access with ID Tokens

When working with authenticated users, proper token management is crucial. While Amplify automatically handles access tokens for data API requests, some scenarios require manual token management for accessing user attributes in your backend services.

 1import { fetchAuthSession } from 'aws-amplify/auth';
 2
 3const session = await fetchAuthSession();
 4if (!session.tokens?.idToken) throw new Error('User not signed in');
 5
 6await client.mutations.action({
 7  ...formData,
 8}, {
 9  authMode: 'userPool',
10  headers: {
11    'Authorization': session.tokens.idToken.toString(),
12  }
13});

In the backend, you can use the email attribute of the user like below if you are using AppSync JS resolver:

1import { util } from '@aws-appsync/utils';
2
3export function request(ctx) {
4  const owner = ctx.identity.claims.email || ctx.identity.username;
5}
6
7export function response(ctx) {
8  return ctx.result;
9} 

Tip 4: Mastering UI Development

Amplify UI provides a rich set of components designed for seamless integration. Learn how to maintain a consistent look and feel when combining Amplify UI with other popular libraries like Material-UI (MUI).

 1import { ThemeProvider, createTheme, defaultDarkModeOverride } from '@aws-amplify/ui-react';
 2import { styled, ThemeProvider as MUIThemeProvider, createTheme } from '@mui/material/styles';
 3
 4const theme = createTheme({
 5    name: 'christmas-theme',
 6    tokens: {
 7      colors: {
 8        background: {
 9          primary: { value: '#FFFFFF' },   // Snow white background
10          secondary: { value: '#165B33' }, // Christmas green
11        },
12      },
13      components: {
14        button: {
15          primary: {
16            backgroundColor: { value: '#CC231E' },
17            color: { value: '#FFFFFF' },
18            _hover: {
19              backgroundColor: { value: '#165B33' },
20            },
21          },
22        },
23      },
24    },
25    overrides: [defaultDarkModeOverride]
26});
27
28const muiTheme = createTheme({
29  palette: {
30    primary: {
31      main: theme.tokens.colors.font.interactive.value,
32    },
33  },
34});
35
36export default function RootLayout({
37  children,
38}: {
39  children: React.ReactNode;
40}) {
41  return (
42    <html lang="en" className={inter.className}>
43      <head>
44        <meta name="viewport" content="width=device-width, initial-scale=1" />
45      </head>
46      <body>
47        <ThemeProvider theme={theme}>
48          <AmplifyProvider>
49            <main className="min-h-screen">
50              <MUIThemeProvider theme={muiTheme}>
51                {children}
52              </MUIThemeProvider>
53            </main>
54          </AmplifyProvider>
55        </ThemeProvider>
56      </body>
57    </html>
58  );
59}

Tip 5: Extending Infrastructure with CDK

For complex backend requirements, AWS CDK enables powerful customization of your Amplify backend. This allows you to manage all types of AWS resources while benefiting from the extensive CDK construct ecosystem.

1(backend.leagueHandler.resources.lambda.node.defaultChild as CfnFunction).addPropertyOverride('LoggingConfig', {
2  LogFormat: 'JSON',
3  ApplicationLogLevel: process.env.PRODUCTION ? 'WARN' : 'TRACE',
4  SystemLogLevel: 'INFO',
5});

Tip 6: Optimizing DynamoDB Access

Learn how to handle common challenges like circular dependencies when accessing DynamoDB tables from Lambda resolvers in your Amplify-generated AppSync API. You can access user identity information in your resolvers using AppSync identity context.

 1import { defineBackend } from '@aws-amplify/backend';
 2import { auth } from './auth/resource';
 3import { data, leagueHandler } from './data/resource';
 4export const backend = defineBackend({
 5  auth,
 6  data,
 7  leagueHandler,
 8});
 9
10const externalTableStack = backend.createStack('ExternalTableStack');
11
12const leagueTable = new Table(externalTableStack, 'League', {
13  partitionKey: {
14    name: 'id',
15    type: AttributeType.STRING
16  },
17  billingMode: BillingMode.PAY_PER_REQUEST,
18  removalPolicy: RemovalPolicy.DESTROY,
19});
20
21backend.data.addDynamoDbDataSource(
22  "ExternalLeagueTableDataSource",
23  leagueTable as any
24);
25
26leagueTable.grantReadWriteData(backend.leagueHandler.resources.lambda);
27(backend.leagueHandler.resources.lambda as NodejsFunction).addEnvironment('LEAGUE_TABLE_NAME', leagueTable.tableName);
declare the DynamoDB table outside of generated Amplify stack in amplify/backend.ts
1const schema = a.schema({
2  League: a.customType({
3    id: a.string().required(),
4    leagueCountry: a.ref('LeagueCountry'),
5    teams: a.ref('Team').array(),
6    season: a.integer(),
7  }),
8});
Declare the DynamoDB schema as a custom type for AppSync in amplify/data/resource.ts, see here for more details.

Tip 7: Building Resilient AI Features

Improve your application's reliability by implementing cross-region model inference with the Amplify AI Kit. While not supported out-of-the-box, you can achieve this using CDK Interoperability.

Hack the role of Lambda function for conversation and AppSync resolver role for generate in amplify/backend.ts

 1function createBedrockPolicyStatement(currentRegion: string, accountId: string, modelId: string, crossRegionModel: string) {
 2  return new PolicyStatement({
 3    resources: [
 4      `arn:aws:bedrock:*::foundation-model/${modelId}`,
 5      `arn:aws:bedrock:${currentRegion}:${accountId}:inference-profile/${crossRegionModel}`,
 6    ],
 7    actions: ['bedrock:InvokeModel*'],
 8  });
 9}
10
11if (CROSS_REGION_INFERENCE && CUSTOM_MODEL_ID) {
12  const currentRegion = getCurrentRegion(backend.stack);
13  const crossRegionModel = getCrossRegionModelId(currentRegion, CUSTOM_MODEL_ID);
14  
15  // [chat converstation]
16  const chatStack = backend.data.resources.nestedStacks?.['ChatConversationDirectiveLambdaStack'];
17  if (chatStack) {
18    const conversationFunc = chatStack.node.findAll()
19      .find(child => child.node.id === 'conversationHandlerFunction') as IFunction;
20
21    if (conversationFunc) {
22      conversationFunc.addToRolePolicy(
23        createBedrockPolicyStatement(currentRegion, backend.stack.account, CUSTOM_MODEL_ID, crossRegionModel)
24      );
25    }
26  }
27
28  // [insights generation]
29  const insightsStack = backend.data.resources.nestedStacks?.['GenerationBedrockDataSourceGenerateInsightsStack'];
30  if (insightsStack) {
31    const dataSourceRole = insightsStack.node.findChild('GenerationBedrockDataSourceGenerateInsightsIAMRole') as IRole;
32    if (dataSourceRole) {
33      dataSourceRole.attachInlinePolicy(
34        new Policy(insightsStack, 'CrossRegionInferencePolicy', {
35          statements: [
36            createBedrockPolicyStatement(currentRegion, backend.stack.account, CUSTOM_MODEL_ID, crossRegionModel)
37          ],
38        }),
39      );
40    }
41  }
42}

Specify the model ID in amplify/data/resource.ts

 1const schema = a.schema({
 2  generateInsights: a.generation({
 3    aiModel: CROSS_REGION_INFERENCE ? {
 4      resourcePath: getCrossRegionModelId(getCurrentRegion(undefined), CUSTOM_MODEL_ID!),
 5     } : a.ai.model(LLM_MODEL),
 6    systemPrompt: LLM_SYSTEM_PROMPT,
 7    inferenceConfiguration: {
 8      maxTokens: 1000,
 9      temperature: 0.65,
10    },
11  })
12  .arguments({
13    requirement: a.string().required(),
14    })
15    .returns(a.customType({
16      insights: a.string().required(),
17    }))
18    .authorization(allow => [allow.authenticated()]),
19
20  chat: a.conversation({
21    aiModel: CROSS_REGION_INFERENCE ? {
22      resourcePath: getCrossRegionModelId(getCurrentRegion(undefined), CUSTOM_MODEL_ID!),
23     } : a.ai.model(LLM_MODEL),
24    systemPrompt: FOOTBALL_SYSTEM_PROMPT,
25  }).authorization(allow => allow.owner()),
26});

Tip 8: Creating Sophisticated Chat Interfaces

The AIConversation component provides a flexible foundation for building chat applications. Master state management and user context handling for multiple conversations.

 1import { useState } from 'react';
 2import { Fab, Paper, IconButton, Box, Tooltip, Typography } from '@mui/material';
 3import { AIConversation } from '@aws-amplify/ui-react-ai';
 4import { Avatar } from '@aws-amplify/ui-react';
 5import '@aws-amplify/ui-react/styles.css';
 6import { generateClient } from 'aws-amplify/data';
 7import { createAIHooks } from '@aws-amplify/ui-react-ai';
 8import { type Schema } from '../../amplify/data/resource';
 9import ReactMarkdown from 'react-markdown';
10
11const client = generateClient<Schema>({ authMode: 'userPool' });
12const { useAIConversation } = createAIHooks(client);
13
14interface ChatBotProps {
15  chatId?: string;
16  refreshKey: number;
17  onStartNewChat: () => void;
18  onLoadConversations: () => void;
19  isLoading: boolean;
20}
21
22export default function ChatBot({ 
23  chatId,
24  refreshKey,
25  onStartNewChat,
26  onLoadConversations,
27  isLoading 
28}: ChatBotProps) {
29  const [open, setOpen] = useState(refreshKey > 0);
30  const [position, setPosition] = useState({ x: 0, y: 0 });
31
32  const conversation = useAIConversation('chat', {
33    id: chatId,
34  });
35  const [{ data: { messages }, isLoading: isLoadingChat }, sendMessage] = conversation;
36  
37  const handleOpen = () => {
38    setOpen(true);
39    onLoadConversations();
40  };
41
42  const handleClose = () => setOpen(false);
43
44  const handleNewChat = () => {
45    // Reset conversation and create new chat
46    onStartNewChat();
47  };
48
49  return (
50<Box sx={{ flexGrow: 1, overflow: 'hidden' }}>
51  <AIConversation
52    key={chatId}
53    allowAttachments
54    messages={messages}
55    handleSendMessage={sendMessage}
56    isLoading={isLoadingChat || isLoading}
57    avatars={{
58      user: {
59        avatar: <Avatar size="small" alt={email} />,
60        username: 'People'
61      },
62      ai: {
63        avatar: <Avatar size="small" alt="AI" />,
64        username: 'Chat Bot'
65      }
66    }}
67    messageRenderer={{
68      text: ({ text }) => <ReactMarkdown>{text}</ReactMarkdown>,
69    }}
70  />
71</Box>
72  );
73}

Tip 9: Streamlining Deployment Debugging

When troubleshooting deployment issues in Amplify Hosting, leverage the --debug flag for deeper insights into pipeline failures, especially when code works in sandbox but fails in production.

 1version: 1
 2backend:
 3  phases:
 4    build:
 5      commands:
 6        - nvm install 20
 7        - nvm use 20
 8        - npm ci --cache .npm --prefer-offline
 9        - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID --debug
10frontend:
11  phases:
12    preBuild:
13      commands:
14        - nvm install 20
15        - nvm use 20
16    build:
17      commands:
18        - npm run build
19  artifacts:
20    baseDirectory: .next
21    files:
22      - '**/*'
23  cache:
24    paths:
25      - .next/cache/**/*
26      - .npm/**/*

Conclusion

AWS Amplify Gen 2 represents a significant evolution in fullstack development on AWS, offering a developer experience comparable to platforms like Vercel with Next.js. These tips will help you leverage Amplify's generated services alongside CDK's powerful constructs to build sophisticated serverless applications efficiently. The platform's seamless integration with the AWS ecosystem makes it an excellent choice for teams looking to accelerate their development process while maintaining enterprise-grade quality and scalability.