How to Connect Next.js Frontend with Amplify Backend Using AWS Amplify Gen 2

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

how to connect Next.js frontend with Amplify backend

Introduction

In our exploration of Hero FAQ’s backend, we explained in detail, the authentication, data models, and serverless logic powered by AWS Amplify Gen 2. Now, let’s bring it all together by exploring how to connect Next.js frontend with Amplify backend, ensuring a smooth and secure bridge between client-side logic and cloud-powered infrastructure.

This article walks you through the core frontend setup, demonstrating how we leverage amplify_outputs.json for configuration, setting up authentication context in next.js with Amplify, manage user authentication flows, and interact with our GraphQL API for dynamic data operations for successful frontend integration with AWS Amplify Gen 2.

The Crucial Link: Initializing Amplify and Establishing Authentication Context

The amplify_outputs.json file is the essential bridge. Generated automatically during your backend deployment, it contains all the necessary connection details for your AWS services like Cognito (for authentication) and AppSync (for your GraphQL API). Without this file, your frontend wouldn’t know where to send its requests in the cloud.

In Hero FAQ, this configuration is loaded right when the Next.js application starts. This ensures that the Amplify client-side libraries are properly initialized and ready to communicate with your deployed backend.

				
					// layout.tsx (simplified)
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";
import Auth from "@/components/auth/Auth"; // Our custom Auth context wrapper

// Configure Amplify using the generated outputs file
Amplify.configure(outputs);

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {/* Auth component provides global authentication context to all children */}
        <Auth>
          {children}
        </Auth>
      </body>
    </html>
  );
}

				
			

Explanation: By simply passing the outputs object to Amplify.configure(), the entire frontend application is equipped to interact with your specific cloud backend. Building on this, the Auth component plays a vital role.

Centralizing Authentication State with Auth.tsx

While we use custom UI for our sign-in/sign-up forms, the @aws-amplify/ui-react library provides an Authenticator.Provider that is incredibly useful for managing and sharing the authentication state across your entire application. Our Auth.tsx component wraps the application’s children with this provider.

				
					// components/auth/Auth.tsx (simplified)
"use client";

import React from "react";
import { Authenticator } from "@aws-amplify/ui-react";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";

// Re-configure Amplify here with SSR support, important for Next.js
Amplify.configure(outputs, { ssr: true });

const Auth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  // The Authenticator.Provider makes authentication state available globally
  return <Authenticator.Provider>{children}</Authenticator.Provider>;
};

export default Auth;

				
			

Explanation: This Auth component is critical because it utilizes Authenticator.Provider from @aws-amplify/ui-react. This provider creates a React Context that holds the current authentication status (e.g., whether a user is signed in, user attributes, etc.). Any component deeper in the tree can then consume this context (e.g., via useAuthenticator hook from @aws-amplify/ui-react) to react to authentication changes or access user information without prop drilling. The Amplify.configure(outputs, { ssr: true }) call within this client component ensures that server-side rendering (SSR) environments in Next.js also properly initialize Amplify, preventing potential hydration mismatches or issues when re-rendering on the client.

User Onboarding: Seamless Authentication with Amplify Auth

Hero FAQ’s user authentication is powered by AWS Cognito, managed through Amplify’s client-side authentication library. We’ve crafted custom sign-in and sign-up forms to offer a polished user experience, while still relying on Amplify’s secure and robust authentication mechanisms.

The Sign-Up Flow: Registration and Confirmation (components/signup.tsx)

When a new user signs up, the process involves two key steps:

  1. Registration: Submitting basic user details (email, password, name) to Cognito.
  2. Confirmation: Verifying the user’s email address, typically via a One-Time Password (OTP).
				
					// components/signup.tsx (simplified)
import { signUp, confirmSignUp, signOut } from 'aws-amplify/auth';

const Signup = () => {
  const onSubmit = async (data) => {
    try {
      // Step 1: Register the user with Cognito
      const { nextStep } = await signUp({ username: data.email, password: data.password, options: { userAttributes: { email: data.email } } });

      // Important: Prevent automatic login after signup for email verification
      await signOut();

      if (nextStep.signUpStep === 'CONFIRM_SIGN_UP') {
        alert(`OTP sent to: ${nextStep.codeDeliveryDetails.destination}`);
        // ... show OTP input field ...
      }
    } catch (error) { /* ... handle registration errors ... */ }
  };

  const handleConfirmOTP = async () => {
    try {
      // Step 2: Confirm the user's account with the OTP
      const { nextStep } = await confirmSignUp({ username: emailForOTP, confirmationCode });
      if (nextStep.signUpStep === 'DONE') {
        alert('Signup confirmed! Please login.');
        router.push('/signin'); // Redirect to login
      }
    } catch (error) { /* ... handle OTP confirmation errors ... */ }
  };
  // ... rest of component logic and JSX ...
};

				
			

Explanation: The signUp function initiates user registration with Cognito. Critically, we immediately call signOut() after a successful signUp. This ensures that even though the user’s account is created, they are not automatically logged in. Instead, they are prompted to enter an OTP sent to their email, which they confirm using confirmSignUp. This email verification step, which triggers our backend postConfirmation Lambda function, is vital for security and data consistency.

The Sign-In Flow: Authentication and Session Management (components/signin.tsx)

Once a user’s account is confirmed, they can sign in:

				
					// components/signin.tsx (simplified)
import { getCurrentUser, signIn } from 'aws-amplify/auth';

const Signin = () => {
  // Check if a user is already signed in on component load
  useEffect(() => {
    async function checkUser() {
      try {
        await getCurrentUser(); // Attempt to get current authenticated user
        router.replace('/'); // If successful, redirect to the main app
      } catch { /* User not signed in, stay on login page */ }
    }
    checkUser();
  }, []);

  const onSubmit = async (data) => {
    try {
      // Authenticate the user with Cognito
      await signIn({ username: data.email, password: data.password });

      // After successful sign-in, retrieve user details
      const user = await getCurrentUser();
      // Store userId in a cookie for basic session context (optional, but useful)
      document.cookie = `userId=${user.userId}; path=/; max-age=86400`; // 1 day expiry

      router.push('/'); // Redirect to the main application
    } catch (error) { /* ... handle login errors ... */ }
  };
  // ... rest of component logic and JSX ...
};

				
			

Explanation: The signIn function handles the login process, verifying credentials against Cognito. Upon successful authentication, getCurrentUser() retrieves the authenticated user’s session and attributes. We then extract the userId (which corresponds to Cognito’s sub attribute) and store it in a browser cookie. This userId is crucial for linking user actions to their profile in our data models. A useEffect hook ensures that users who are already logged in are automatically redirected, enhancing user experience.

Dynamic Data Management: Interacting with the GraphQL API

Hero FAQ’s frontend interacts extensively with the AWS AppSync GraphQL API (which was provisioned by Amplify Data). This interaction is how we perform all our Create, Read, Update, and Delete (CRUD) operations on Forms, Questions, and Answers. We achieve this through custom React Query hooks that wrap a pre-configured Amplify GraphQL client (client_with_token).

Creating Content: Adding New FAQs and Questions (components/questionsform.tsx)

This component empowers administrators to define new FAQ forms and add questions to them.

				
					// components/questionsform.tsx (simplified)
import { useAddFaqMutation, useAddQuestionMutation } from "@/hooks/useAddFaqMutations";
import { v4 as uuidv4 } from "uuid"; // For unique IDs
import { useQueryClient } from "@tanstack/react-query";
import { getCurrentUser } from 'aws-amplify/auth';

export default function QuestionForm({ onSubmitSuccess }) {
  const [title, setTitle] = useState('');
  const [questions, setQuestions] = useState([]);
  const [userId, setUserId] = useState(null); // Creator's ID

  // Fetch the currently authenticated user's ID
  useEffect(() => { /* ... getCurrentUser().then(user => setUserId(user.username)) ... */ }, []);

  // React Query mutations for adding forms and questions
  const { mutateAsync: submitFaqForm } = useAddFaqMutation();
  const { mutateAsync: submitQuestion } = useAddQuestionMutation();
  const queryClient = useQueryClient(); // Used for cache invalidation

  const handleSubmitAll = async () => {
    // ... input validation ...
    const formId = uuidv4(); // Generate a unique ID for the new form

    try {
      // 1. Create the Form entry in the database
      await submitFaqForm({ input: { formId, title, userId } });

      // 2. Iterate and create each Question entry, linking it to the new form
      for (const q of questions) {
        await submitQuestion({ input: { formId, questionId: uuidv4(), question: q.question, options: q.options, userId } });
      }

      // Invalidate the cache to trigger a refresh of the FAQ list in the sidebar
      queryClient.invalidateQueries({ queryKey: ["faqList"] });

      alert("Form and questions submitted successfully!");
      onSubmitSuccess(); // Callback to reset UI
    } catch (error) {
      console.error("Failed to create form:", error);
      alert("Error submitting form");
    }
  };
  // ... JSX for form inputs ...
}

				
			

Explanation: This component demonstrates how our custom useAddFaqMutation and useAddQuestionMutation hooks are invoked. These hooks, behind the scenes, execute GraphQL mutations defined in our generated graphql/mutations.ts file. The mutations send data (like formId, title, question, options, and crucially, the userId of the creator) to the AppSync API, which then stores them in the DynamoDB tables. The use of uuidv4() ensures unique IDs for each new record, and queryClient.invalidateQueries() is a React Query feature that tells the app to refetch outdated data (like the list of forms in the sidebar) after a successful change.

Reading Content: Displaying User Answers (components/userslist.tsx)

This component allows administrators to review all the forms and their corresponding answers submitted by a specific user.

				
					// components/userslist.tsx (simplified)
import { listAnswerModels, getQuestionModel, listQuestionModelByFormId } from "@/app/graphql/queries";
import { client_with_token } from "@/utils/amplifyGenerateClient"; // Our authenticated GraphQL client

export default function UserList({ userId }) {
  const [answers, setAnswers] = useState([]);
  const [formDataMap, setFormDataMap] = useState({});

  useEffect(() => {
    async function fetchAnswersAndForms() {
      try {
        // Step 1: Fetch all answers submitted by the specified userId
        const res = await client_with_token.graphql({
          query: listAnswerModels,
          variables: { filter: { userId: { eq: userId } } }, // Filter by userId
        });
        const items = res.data?.listAnswerModels?.items || [];
        setAnswers(items);

        // Step 2: For each answer, retrieve its associated question and form details
        const formMap = {};
        for (const ans of items) {
          if (!ans.questionId) continue;
          const qRes = await client_with_token.graphql({
            query: getQuestionModel,
            variables: { questionId: ans.questionId },
          });
          const questionData = qRes.data?.getQuestionModel;
          const formId = questionData?.formId;
          // ... further logic to fetch all questions for a given formId ...
        }
        setFormDataMap(formMap);
      } catch (err) {
        console.error("Error loading forms/questions/answers", err);
      }
    }
    if (userId) { fetchAnswersAndForms(); }
  }, [userId]);
  // ... JSX for displaying forms and answers ...
}

				
			

Explanation: This component leverages multiple GraphQL queries defined in our generated graphql/queries.ts file. It first uses listAnswerModels with a filter to get all answers linked to a particular userId. Then, for each answer, it performs additional queries (getQuestionModel, listQuestionModelByFormId) to retrieve the full question details and the form it belongs to. This demonstrates how the frontend traverses the relationships defined in our backend data models to gather comprehensive, linked data from AppSync. The client_with_token instance is crucial here, as it ensures that all these GraphQL requests are authenticated with the current user’s session token, granting them the necessary permissions.

Key Takeaways for Frontend-Backend Integration

Beyond the specific components, the frontend integration with AWS Amplify Gen 2 relies on several powerful concepts:

  • Code Generation: Amplify Data automatically generates TypeScript types and GraphQL operations (in your graphql/ folder) directly from your backend schema. This provides end-to-end type safety, ensuring that your frontend code always matches the backend’s data structures, significantly reducing runtime errors.
  • Authentication Context: The @aws-amplify/ui-react Authenticator.Provider and aws-amplify/auth functions (getCurrentUser, signIn, signUp) simplify managing user sessions and injecting authentication information into API calls throughout the application.
  • Client-Side Data Fetching: Custom React Query hooks wrap the Amplify GraphQL client, providing robust caching, loading states, and error handling for data interactions, making the application highly performant and user-friendly.

By initializing Amplify with amplify_outputs.json, establishing a global authentication context, and leveraging auto-generated GraphQL clients for data interaction, Hero FAQ’s frontend integration with AWS Amplify Gen 2 backend is complete. This powerful combination of client-side logic and cloud resources provides a secure, scalable, and dynamic application experience.

In our final article, we’ll delve into the overall dashboard experience and discuss potential future enhancements for Hero FAQ.

Share :

Table of Contents