Diving Deeper: Building a Scalable AWS Amplify Gen 2 Backend for Hero FAQ – Part 2

Partner with Compileinfy to transform your business vision into powerful digital solutions.

Hero FAQ AWS Amplify Gen 2 Backend Setup

Introduction : AWS Amplify Gen 2 Backend

Building and managing cloud infrastructure can be a daunting task, but the AWS Amplify Gen 2 backend simplifies this process by allowing developers to define authentication, data models, and serverless functions entirely in TypeScript, with automatic provisioning and deployment handled for you.

In our previous discussion, we laid the groundwork for building full-stack applications with AWS Amplify Gen 2. Now, we’re diving deeper into the backend of the Hero FAQ project, a real-world implementation that showcases how Amplify Gen 2 enables a seamless developer experience. This article offers a comprehensive breakdown of the core backend elements, including our Cognito-based authentication setup, custom data models in DynamoDB, serverless business logic using AWS Lambda, and the pivotal amplify_outputs.json file that connects your backend services to the frontend.

Whether you’re building your first full-stack application or optimizing an existing project, this guide will help you understand how to design and scale a robust Amplify Gen 2 backend the right way.

How to set up an Authentication with AWS Amplify Gen 2

The auth/resource.ts file is the control center for Hero FAQ’s authentication and authorization, defining how users interact with the system and what permissions they hold. Our strategy centers on email-based login and Cognito User Groups for robust Role-Based Access Control (RBAC).

Let’s examine the configuration:

				
					// amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';
import { postConfirmation } from './post-confirmation/resource';
import { profileGroups } from './profileGroups';

/**
 * Define and configure your auth resource
 * @see https://docs.amplify.aws/gen2/build-a-backend/auth
 */
export const auth = defineAuth({
  loginWith: {
    email: true,
  },
  groups: Object.values(profileGroups),
  triggers: {
    postConfirmation
  },
  access: (allow) => [
    allow.resource(postConfirmation).to(['addUserToGroup'])
  ]
});

				
			
  • loginWith: { email: true }: Configures Amazon Cognito User Pools for email-based sign-up and sign-in.
  • groups: Object.values(profileGroups): Dynamically creates Cognito user groups (e.g., ADMIN, USER) for RBAC.
  • triggers: { postConfirmation }: Specifies a Lambda function to execute after user account confirmation, ideal for post-registration tasks.
  • access: (allow) => […]: Grants the postConfirmation Lambda function necessary IAM permissions to add users to Cognito groups, automating role assignments.

Implementing Serverless Business Logic: The Post-Confirmation Lambda Trigger

The postConfirmation Lambda function executes vital logic right after a user confirms their account. For Hero FAQ, its primary responsibilities are creating a corresponding user profile in our database and assigning the new user to a default group.

Lambda Function Definition (amplify/auth/post-confirmation/resource.ts)

First, we define the Lambda function resource:

				
					// amplify/auth/post-confirmation/resource.ts
import { defineFunction } from '@aws-amplify/backend';

export const postConfirmation = defineFunction({
  name: 'post-confirmation',
});

				
			

This simple definition tells Amplify to create an AWS Lambda function named post-confirmation.

The Handler Implementation (amplify/auth/post-confirmation/handler.ts)

Next, we write the actual logic:

				
					// amplify/auth/post-confirmation/handler.ts
import type { PostConfirmationTriggerHandler } from "aws-lambda";
import { type Schema } from "../../data/resource";
import { Amplify } from "aws-amplify";
import { generateClient } from "aws-amplify/data";
import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime';
import { env } from "$amplify/env/post-confirmation";
import { profileGroups } from './../profileGroups';
import {
    CognitoIdentityProviderClient,
    AdminAddUserToGroupCommand
} from '@aws-sdk/client-cognito-identity-provider';

// Configure Amplify client for Lambda environment
const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(env);
Amplify.configure(resourceConfig, libraryOptions);

const client = generateClient<Schema>();
const cognitoClient = new CognitoIdentityProviderClient();

async function addUserToGroup(username: string, userPoolId: string): Promise<void> {
    const command = new AdminAddUserToGroupCommand({
        GroupName: profileGroups.USER, // Assign new users to the default 'USER' group
        Username: username,
        UserPoolId: userPoolId
    });

    try {
        const response = await cognitoClient.send(command);
        console.log("User added to Cognito group", { requestId: response.$metadata.requestId });
    } catch (error) {
        console.error("Failed to add user to group:", error);
        throw error; // Re-throw to ensure the error is logged and potentially retried
    }
}

export const handler: PostConfirmationTriggerHandler = async (event) => {
    const userId = event.request.userAttributes.sub;
    const email = event.request.userAttributes.email;
    const username = event.userName;
    const userPoolId = event.userPoolId;

    console.log("Post confirmation event received:", { userId, email, username, userPoolId });

    // Track operations for better error handling and transparency
    const operations = {
        userCreated: false,
        groupAssigned: false
    };

    try {
        // Create user profile in our database (DynamoDB via Amplify Data)
        await client.models.userModel.create({
            userId,
            email,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString()
        });
        operations.userCreated = true;
        console.log("User profile created in data store");

    } catch (error) {
        console.error("Error creating user profile in data store:", error);
        // Important: Do NOT re-throw here. If user profile creation fails,
        // we still want the user's Cognito account to be confirmed.
    }

    try {
        // Add user to the default Cognito group
        await addUserToGroup(username, userPoolId);
        operations.groupAssigned = true;
    } catch (groupError) {
        console.error("Failed to add user to default group:", groupError);
        // Do NOT re-throw. Group can be manually assigned later if this fails.
    }

    // Log the final status of all post-confirmation operations
    console.log("Post-confirmation operations summary:", operations);

    // Always return the event to allow successful user registration completion
    return event;
};

				
			
  • Amplify Client Configuration: Enables the Lambda function to interact with other Amplify backend resources.
  • addUserToGroup function: Uses the AWS SDK to programmatically add a confirmed user to a specified Cognito User Group (profileGroups.USER).
  • Main Handler Logic: Extracts user details, creates a new userModel entry in the database, and then assigns the user to their default group.
  • Error Handling: Errors are logged but not re-thrown to ensure the user’s core registration process in Cognito is not interrupted.

Structuring Data with Amplify Data Models: amplify/data/resource.ts and models/

The data/resource.ts file, coupled with individual model definitions in the models/ directory, forms the blueprint for Hero FAQ’s data layer. Amplify Data automatically provisions an AWS AppSync GraphQL API and underlying Amazon DynamoDB tables based on these TypeScript schemas.

Data Resource Definition (amplify/data/resource.ts)

				
					// data/resource.ts
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
import { postConfirmation } from "../auth/post-confirmation/resource";
import { UserModel } from './models/UserModel';
import { QuestionModel } from './models/QuestionModel';
import { AnswerModel } from './models/AnswerModel';
import { FormModel } from './models/FormModel';

const schema = a.schema({
  userModel: UserModel,
  questionModel: QuestionModel,
  answerModel: AnswerModel,
  formModel: FormModel,
}).authorization((allow) => [
  allow.authenticated(), // Allow authenticated users access to the schema
  allow.resource(postConfirmation) // Grant the postConfirmation Lambda access
]);

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool', // Default auth mode is Cognito User Pools
  },
});

				
			
  • schema definition: Defines the GraphQL schema, with each property mapping to a DynamoDB table (e.g., userModel, formModel, questionModel, answerModel).
  • .authorization((allow) => […]): Sets global authorization rules:
    • allow.authenticated(): Ensures only authenticated users can interact with the data.
    • allow.resource(postConfirmation): Grants the Lambda function permissions to read/write data.
  • defaultAuthorizationMode: ‘userPool’: Designates Amazon Cognito User Pools as the default authorization method.
Deep Dive into Data Models: models/ Directory

Let’s break down each individual data model’s structure and relationships.

				
					// models/AnswerModel.ts
import { a } from '@aws-amplify/backend';

export const AnswerModel = a
  .model({
    answerId: a.id(),
    questionId: a.id(),
    userId: a.id(),
    formId: a.id().required(),
    selectedOptions: a.string().array().required(),
    questions: a.belongsTo('questionModel', 'questionId'),
    answeredby: a.belongsTo('userModel', 'userId'),
    createdAt: a.datetime(),
    updatedAt: a.datetime(),
  }).identifier(["answerId"])
  .secondaryIndexes(index => [
    index("formId").name("AnswersByFormId"), // Optimize queries for answers by form
  ]);
				
			
  • Defines answers with unique answerId, foreign keys for questionId, userId, formId, and stores selectedOptions.
  • Establishes belongsTo relationships with QuestionModel and UserModel.
  • Includes a secondary index on formId for efficient querying.

models/FormModel.ts

				
					// FormModel.ts
import { a } from '@aws-amplify/backend';

export const FormModel = a
  .model({
    formId: a.id(),
    title: a.string().required(),
    userId: a.id().required(), // Creator of the form
    createdAt: a.datetime(),
    updatedAt: a.datetime(),
    formQuestions: a.hasMany('questionModel', 'formId'), // Forms have many questions
    createdBy: a.belongsTo('userModel', 'userId'), // Link to the user who created it
  }).identifier(["formId"]);
				
			
  • Defines forms with unique formId, title, and userId (creator).
  • Sets up a hasMany relationship with QuestionModel and a belongsTo relationship with UserModel.

models/QuestionModel.ts

				
					// QuestionModel.ts
import { a } from '@aws-amplify/backend';

export const QuestionModel = a
  .model({
    questionId: a.id(),
    question: a.string().required(),
    userId: a.id().required(), // Creator of the question
    formId: a.id().required(), // Form this question belongs to
    options: a.string().array().required(), // Answer options (e.g., for multiple-choice)
    askedby: a.belongsTo('userModel', 'userId'),
    answers: a.hasMany('answerModel', 'questionId'), // Questions have many answers
    form: a.belongsTo('formModel', 'formId'), // Link back to the parent form
    createdAt: a.datetime(),
    updatedAt: a.datetime(),
  }).identifier(["questionId"])
  .secondaryIndexes(index => [index("formId").name("QuestionsByformId")]); // Optimize queries for questions by form
				
			
  • Defines questions with unique questionId, question text, userId (creator), formId, and options.
  • Establishes belongsTo relationships with UserModel and FormModel, and a hasMany relationship with AnswerModel.
  • Includes a secondary index on formId.
models/UserModel.ts
				
					// UserModel.ts
import { a } from '@aws-amplify/backend';

export const UserModel = a
  .model({
    userId: a.id(), // Corresponds to Cognito 'sub' attribute
    firstname: a.string(),
    lastname: a.string(),
    email: a.email().required(),
    createdAt: a.datetime(),
    updatedAt: a.datetime(),
    questions: a.hasMany('questionModel', 'userId'), // User asked many questions
    answers: a.hasMany('answerModel', 'userId'),    // User submitted many answers
    forms: a.hasMany('formModel', 'userId'),          // User (admin) created many forms
  }).identifier(["userId"]);
				
			
  • Defines user profiles with userId (mapping to Cognito’s sub), optional name fields, and a required email.
  • Sets up hasMany relationships for questions, answers, and forms to link users to their associated data.

The Crucial Link: amplify_outputs.json

After deploying your backend, Amplify generates the amplify_outputs.json file. This file acts as the central configuration bridge between your deployed cloud backend and your frontend application. This JSON file contains all necessary connection parameters, including Auth, Data, and Storage details. Your frontend application will read this file programmatically to configure the Amplify client libraries, eliminating the need to hardcode sensitive cloud resource identifiers.

Automatically Generated GraphQL Assets (graphql/)

Amplify Data automatically generates GraphQL artifacts in your graphql/ directory based on your a.model() definitions.

  • graphql/API.ts: Contains TypeScript type definitions for your schema, enabling end-to-end type safety in your frontend.
  • graphql/mutations.ts: Defines GraphQL mutation operations (Create, Update, Delete).
  • graphql/queries.ts: Contains GraphQL query operations for reading data.
  • graphql/subscriptions.ts: Lists GraphQL subscription operations for real-time data updates.

These generated files are directly consumable by your frontend, allowing type-safe interaction with your backend data.

Common Pitfalls and Solutions in AWS Amplify Gen 2 Backend Development

Understanding common issues can save significant debugging time.

1. Schema Deployment Failures
  • Problem: Changes to data models fail to deploy.
  • Cause: Adding required fields without default values to existing models with data, circular references, or invalid field types.
  • Solution:
    • Make new fields optional or provide a default value if modifying existing models.
    • Review relationships for circular dependencies.
2. Permission and Authorization Errors
  • Problem: Users cannot access data or perform operations.
  • Cause: Incorrect authorization rules or missing Lambda permissions.
  • Solution: Implement layered authorization rules (allow.authenticated(), allow.groups(), allow.owner()) and ensure Lambda functions have explicit allow.resource() permissions.
3. Type Generation Problems
  • Problem: Frontend TypeScript types (graphql/API.ts) don’t match or are missing.
  • Cause: Not regenerating types after schema changes or local caching.
  • Solution:
    • Always run npx ampx generate graphql-client-code after schema changes.
    • Clear TypeScript cache (npx tsc –build –clean) and restart your dev server.
4. Lambda Function Configuration and Execution Issues
  • Problem: Lambda functions fail silently or cannot access resources.
  • Cause: Missing environment variables, incorrect IAM permissions, or timeouts.
  • Solution:
    • Robust Logging: Use detailed console.log statements.
    • Non-blocking Errors: For triggers like postConfirmation, always return the event object to prevent blocking Cognito flow.
    • Environment Variables: Access using $amplify/env/function-name.
5. Data Model Relationship Errors
  • Problem: Relationships (hasMany, belongsTo) don’t work.
  • Cause: Mismatched foreign keys or incorrect definitions.
  • Solution: Ensure consistent foreign key names and relationship types across models.

What's Next: Connecting AWS Amplify Gen 2 Backend to Frontend

We’ve now thoroughly explored AWS Amplify Backend Setup for Hero FAQ’s backend, from user authentication and data models to serverless logic and common pitfalls. The amplify_outputs.json file is our crucial link.

In the next article, we’ll shift our focus entirely to the frontend. You’ll learn exactly how to leverage these deployed backend resources and the amplify_outputs.json file to connect your frontend application to your Amplify backend, making authenticated API calls and rendering dynamic data.

Ready to see how Amplify Gen 2 seamlessly connects your frontend to this powerful backend? 

Share :

Table of Contents