DEV Community

loading...
Cover image for Complete Guide to Amplify and Next.js

Complete Guide to Amplify and Next.js

dev_sahan profile image Sahan Amarsha Updated on ・15 min read

Hello Everyone, this is the complete blog post for our Amplify + Next.js video series. So in this series, we will be building a fullstack web application with Amplify and Next.js.

Click here to see the video walkthrough.

Table of Content

  1. Introduction
  2. Setting Up the Project
  3. Cognito Authentication
  4. AppSync API
  5. S3 Storage

Introduction

What We Will Build?


App Preview

We will build a simple profile app. Through this app, you can understand how to use AWS Cognito Authentication, AWS AppSync Backend API, and Amazon Simple Storage Service (S3).

Why Next.js?

Next.js is a React Static Site Generation Web Framework built by Vercel. Next.js introduced server-side React. Next.js has some cool features such as pre-rendering data-fetching methods, more on that later.

Also, with Next.js you don’t have to create a router by yourself. You can simply create a new file. The route will be automatically generated.

Why Amplify?

Amplify is an AWS Framework that makes developing a full-stack application quite easy. The Amplify Framework provides the following services.

Amplify CLI — Configure all the services needed to power your backend through a simple command-line interface.

Amplify Libraries — Use case-centric client libraries to integrate your app code with a backend using declarative interfaces.

Amplify UI Components — UI libraries for React, React Native, Angular, Ionic, and Vue.

Amplify makes it easy to use AWS Services in a Full Stack App.

First, let’s look at the AWS Architecture Diagram for our Application.

AWS Architecture Diagram


AWS Architecture Diagram for our Profile App

You will get the gist of these services as we go on with the project. Don’t worry Amplify makes it much easier to work with these different services.

We will use an S3 Bucket to store our profile image. Amazon Cognito will handle Authentication. We will use AWS AppSync to develop our GraphQL API. Our data will be stored in Amazon DynamoDB, NoSQL Database.

Ready to build the app? Let’s get started. 🛠️

In order to avoid any disturbances in the future. Make sure you have the following prerequisites installed.

Setting Up the Project

Installing and Configuring Amplify CLI

Through this tutorial, we will work with AWS Amplify CLI. You can install it by running,

npm install -g @aws-amplify/cli@4.39.0
Enter fullscreen mode Exit fullscreen mode

Then you need to run amplify configure. This will set up your Amplify CLI. There you will set up a new IAM User. You will finish setting up your IAM User, by providing the accessKeyId and secretAccessKey for your IAM user.

If you are stuck at some point, you can refer to this original guideline on installing Amplify CLI, https://docs.amplify.aws/cli/start/install

Creating a New Next.js App

Hope you have installed and configured Amplify CLI successfully.

To start the proceedings, let’s start with setting up our Next.js Project. You will need to run these two commands from your project directory.

npm install -g create-next-app
npx create-next-app next-profileapp
Enter fullscreen mode Exit fullscreen mode

This will install create-next-app npm package globally. The second command will create a new Next.js application in the directory next-profileapp. Navigate into that directory, and open our newly created Next.js project in your preferred code editor. You should see a project structure similar to this.


Next.js Project Structure

You can test your project by running the command npm run dev in your project directory. Your project will run at http://localhost:3000.

Initializing Amplify Backend

Now, we need to initialize Amplify for our project. Then we can add services one by one.

In the project directory, run

amplify init
Enter fullscreen mode Exit fullscreen mode

Then you will be prompted for the following information regarding the project you will initialize.


Running amplify init

Just accept the default. You should be good to go. For the AWS Profile, you can choose your default AWS Account, the one we have configured earlier.

When you initialize your Amplify Project,

  • It creates a file called aws-exports.js in the src directory. This file will store all the relevant information to identify the AWS resources/services that we are going to provision.
  • It creates a directory called amplify. This directory will be used to store the templates and configuration details of the services that we are going to use. In this directory, Amplify will hold our backend schema as well.
  • You can use the command, amplify console to access the cloud project’s AWS Amplify Console.

To complete setting up our Amplify Project, we need to configure amplify in a higher-order component. Adding the following lines of code in your App.js or inde.js file will do the job.

import "../styles/globals.css";
import awsExports from "../src/aws-exports";

Amplify.configure({...awsExports, ssr: true });
Enter fullscreen mode Exit fullscreen mode

Adding Authentication

Now, adding Authentication to your Next.js Application gets easier with Amplify. First, you need to include AWS Cognito Authentication Service to your Amplify Backend.

Run amplify add auth, in your console. Submit the following information, when Configuring Authentication using the prompt.


Running amplify add auth

Then, run amplify push, to deploy your backend. Amplify will take care of the rest by creating your Cognito Userpool.

We can use AmplifyAuthenticator Component to add login, signup functionalities in our Next.js Project. First, let’s install Amplify UI Components using npm.

npm install aws-amplify @aws-amplify/ui-react
Enter fullscreen mode Exit fullscreen mode

Navigate to your pages/_app.js file, configure Amplify, and wrap your return Component with AmplifyAuthenticator like this.

Directory: pages/_app.js

import { Amplify } from "aws-amplify";
import { AmplifyAuthenticator } from "@aws-amplify/ui-react";
import awsExports from "../src/aws-exports";
import "../styles/globals.css";
Amplify.configure({ ...awsExports, ssr: true });
function MyApp({ Component, pageProps }) {
  return (
    <AmplifyAuthenticator>
      <Component {...pageProps} />
    </AmplifyAuthenticator>
  );
}
export default MyApp;
Enter fullscreen mode Exit fullscreen mode

When you run your app. This login screen will show up. Try logging in as a new user. This will lead you to the home page. The user that we have created, will be saved in a Cognito User Pool.


Sign-in Screen by Amplify

One issue though. Now we can’t log out. Let’s add a new ‘navigation component’, where we can add a ‘sign out button’.

Adding a Navigation Bar

Before that, let’s add Bootstrap. Since I want to use easy styling, I will be using Bootstrap throughout the tutorial.

Run,

npm install react-bootstrap bootstrap
Enter fullscreen mode Exit fullscreen mode

Also, add this import in your pages/_app.js file.

import 'bootstrap/dist/css/bootstrap.min.css';
Enter fullscreen mode Exit fullscreen mode

Add a component directory. In that directory, add a new file called Navbar.js. Copy and paste the following code.

directory: components/Navbar.js

import Link from "next/link";
import { Auth } from "aws-amplify";
import React from "react";
const Navbar = () => {
  const signOutHandler = () => {};
  return (
    <nav className="navbar w-100 navbar-expand navbar-dark bg-dark mb-4">
      <div className="container">
        <a className="navbar-brand" href="#">
          Profile App
        </a>
        <div className="collapse navbar-collapse">
          <ul className="navbar-nav ml-auto">
            <li className="nav-item">
              <Link href="/">
                <a className="nav-link">Home</a>
              </Link>
            </li>
            <li className="nav-item">
              <Link href="/edit-user">
                <a className="nav-link">Edit User</a>
              </Link>
            </li>
            <button
              className="btn btn-danger"
              type="button"
              onClick={signOutHandler}
            >
              Sign Out
            </button>
          </ul>
        </div>
      </div>
    </nav>
  );
};
export default Navbar;
Enter fullscreen mode Exit fullscreen mode

Please notice that I have used a Sign-out Button in the Navbar. That button should trigger Auth.signOut function provided by Amplify Library. This method will end the user session. Since we are using AmplifyAuthenticator wrapper component, logged-out users will get automatically redirected to Sign-in Screen.

Copy and paste the following code into signOutHandler method.

const signOutHandler = async () => {
    try {
        await Auth.signOut();
    } catch (err) {
      console.log(err);
    }
};
Enter fullscreen mode Exit fullscreen mode

With that, our Navbar is fully completed. Let’s use that Navigation bar on our home page.

directory: pages/index.js

import React from "react";
import Head from "next/head";
import Navbar from "../components/Navbar";

export default function Home() {
  return (
    <div className="w-100 h-100 d-flex flex-column justify-content-start">
      <Head>
        <title>Profile App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Navbar />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

I did clear the default page by Next.js. Try signing out and logging back in. 

Now, we have successfully added Authentication into our Next.js Application. Congratulations on completing the first part of the tutorial! 


Now into the next part, we will complete our project by adding our AppSync API, and an S3 Bucket.

Adding AppSync API

Let’s store some details on the user by adding an AppSync API to our Next.js application. Users can add first name, last name, and some description. The user should be able to add a profile image as well. We will add the profile image functionality in the next part.

As I’ve said earlier, through AppSync, we can build a GraphQL API. All the heavy lifting such as connecting and creating DynamoDB Tables, generating queries and mutations, will be done by AppSync.

Executing 'amplify add api'

Using AppSync gets easier with Amplify. First, let’s add the AppSync API to our app.

Run,

amplify add api
Enter fullscreen mode Exit fullscreen mode

Accept the default configurations.


Adding AppSync API

Editing GraphQL Schema

Navigate into amplify/backend/api/schema.graphql file. This is where we will define our GraphQL schema. Let’s add a simple user schema. Copy and paste the following schema.

type User @model {
  id: ID!
  firstName: String
  lastName: String
  description: "String"
  image: String
}
Enter fullscreen mode Exit fullscreen mode

Save the schema.graphql file. Run amplify push to push your changes into Amplify Backend.


Running amplify push after adding AppSync API

Now, our AppSync API has been created. Also, the AppSync Library automatically created queries, mutations for our GraphQL Schema. Run amplify api console to view your AppSync API in AWS.

You could play around with some GraphQL operations in this AWS AppSync Console.


AWS AppSync Console

Adding Edit User Page

Let’s start interacting with our AppSync API.

First, create a new page to edit user details. Create a new file callededit-user.js in the pages directory. Copy and paste the following code.

Directory: pages/edit-user.js

import React, { useState } from "react";
import { Form } from "react-bootstrap";
import { createUser, updateUser } from "../src/graphql/mutations";
import { API } from "@aws-amplify/api";
import { Auth } from  "@aws-amplify/auth";
import Navbar from "../components/Navbar";

const EditUser = () => {
    const [firstName, setFirstName] = useState('');
    const [secondName, setSecondName] = useState('');
    const [description, setDescription] = useState('');
    const submitHandler = async (event) => {
        event.preventDefault();
        // Save Details
    };
    return (
        <div className="d-flex flex-column justify-content-center w-100 h-100">
            <Navbar />
            <h1 className="align-self-center">Edit User Details</h1>
            <Form className="w-50 align-self-center">
                <Form.Group className="mt-2" controlId="firstName">
                    <Form.Label>First Name</Form.Label>
                    <Form.Control
                        type="text"
                        value={firstName}
                        placeholder="Enter Your First Name"
                        onChange={(event) => {
                            setFirstName(event.target.value);
                        }}
                    />
                </Form.Group>
                <Form.Group className="mt-2" controlId="secondName">
                    <Form.Label>Second Name</Form.Label>
                    <Form.Control
                        type="text"
                        value={secondName}
                        placeholder="Enter Your Second Name"
                        onChange={(event) => {
                            setSecondName(event.target.value);
                        }}
                    />
                </Form.Group>
                <Form.Group className="mt-2" controlId="description">
                    <Form.Label>Description</Form.Label>
                    <Form.Control
                        as="textarea"
                        value={description}
                        rows={5}
                        placeholder="Enter Your Description"
                        onChange={(event) => {
                            setDescription(event.target.value);
                        }}
                    />
                </Form.Group>
                <button
                    type="submit"
                    onClick={submitHandler}
                    className="btn btn-primary"
                >
                    Submit
                </button>
            </Form>
        </div>
    );
};
export default EditUser;
Enter fullscreen mode Exit fullscreen mode

This is a simple bootstrap form along with some state variables. Now you can navigate to ‘Edit User Page’ using the navbar. It should look something like this.


Uploaded Profile Image Preview

Now at the press of this submit button we need to save entered details. In order to do that, we need to call CreateUser GraphQL mutation.

Adding 'submitHandler' Function

Copy and paste the following code into submitHandler function.

const submitHandler = async (event) => {    
    event.preventDefault();
    const currentUser = await Auth.currentAuthenticatedUser();
    try {
        const result = await API.graphql({
            query: createUser,
            variables: {
                input: {
                    id: currentUser.attributes.sub,
                    firstName: firstName,
                    lastName: secondName,
                    description: "description,"
                },
            },
        });
        console.log(result);
    } catch (err) {
        console.log(err);
    }
};
Enter fullscreen mode Exit fullscreen mode

Now, you can test this. Fill in the inputs and press enter. You could try querying from our AppSync Console.

Auth.currentAuthenticatedUser() will do exactly what the name suggests. It will return details about the logged-in user. Cognito gives every user an attribute called sub, a unique string value. We can use this value as the unique id in our User Table.

Then I have called AppSync GraphQL API in order to perform ‘createUser’ mutation. This mutation will create a new record in our User Table.

Fetching Data in Pre-rendering Stage

Now let’s examine a user scenario here. If I’m a new user I should be able to add my details to this App. If I have already added the details, I should be able to edit my information as well. Okay, let’s build this.

I will use the getServerSideProps method provided by Next.js. Through this method, we can fetch data on each request. Learn more about Next.js data fetching here.

Copy and paste the following code into getServerSideProps method. Make sure to add the imports beforehand.

import { getUser } from "../src/graphql/queries";
import {withSSRContext} from "aws-amplify";

export async function getServerSideProps({ req, res }) {
  const { Auth, API } = withSSRContext({ req });
  try {
    const user = await Auth.currentAuthenticatedUser();
    const response = await API.graphql({
      query: getUser,
      variables: { id: user.attributes.sub },
    });

  if (response.data.getUser) {
      return {
        props: {
          mode: "EDIT",
          user: response.data.getUser,
          error: false,
        },
      };
    } else {
      return {
        props: {
          mode: "ADD",
          error: false,
        },
      };
    }
  } catch (err) {
    console.log(err);
    return {
      props: {
        error: true,
      },
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

This getStaticProps method will return an object with 3 fields.

01. mode — String If the user is new, the mode is ‘ADD’, otherwise it is ‘EDIT.

02. user — JSON Object Already added user details to preview initial values to the user, before editing.

03. error — Boolean If something goes wrong, ‘true’. We will use this boolean to display something readable to the user.

If you look closely, you should see that I’m querying user details in this method.

const user = await Auth.currentAuthenticatedUser();
const response = await API.graphql({
  query: getUser,
  variables: { id: user.attributes.sub },
});
Enter fullscreen mode Exit fullscreen mode

Then I have called AppSync GraphQL API with ‘getUser’ query. ‘getUser’ query will search through the DynamoDB to find a record with the given id. If there’s none response will return a null object.

Now let’s use those pre-rendered data in our EditPage. We can set initial values and call ‘updateUser’ or ‘createUser’ mutation by considering our Mode.

The final version of EditUser Page is like this. I have updated the submitHandler function, and defined initial values for our state variables.

Directory: pages/edit-user.js

import React, {useState} from "react";
import {Form} from "react-bootstrap";
import {getUser} from "../src/graphql/queries";
import {createUser, updateUser} from "../src/graphql/mutations";
import {withSSRContext} from "aws-amplify";
import {API} from "@aws-amplify/api";
import {Auth} from "@aws-amplify/auth";
import Navbar from "../components/Navbar";


export async function getServerSideProps({req, res}) {
    const {Auth, API} = withSSRContext({req});
    try {
        const user = await Auth.currentAuthenticatedUser();
        const response = await API.graphql({
            query: getUser,
            variables: {id: user.attributes.sub},
        });
        if (response.data.getUser) {
            return {
                props: {
                    mode: "EDIT",
                    user: response.data.getUser,
                    error: false,
                },
            };
        } else {
            return {
                props: {
                    mode: "ADD",
                    error: false,
                },
            };
        }
    } catch (err) {
        console.log(err);
        return {
            props: {
                error: true,
            },
        };
    }
}

const EditUser = ({user, error, mode}) => {
    const [firstName, setFirstName] = useState(mode === 'EDIT' ? user.firstName : '');
    const [secondName, setSecondName] = useState(mode === 'EDIT' ? user.lastName : '');
    const [description, setDescription] = useState(mode === 'EDIT' ? user.description : '');
    const submitHandler = async (event) => {
        event.preventDefault();
        const currentUser = await Auth.currentAuthenticatedUser();
        try {
            const result = await API.graphql({
                query: mode === 'EDIT' ? updateUser : createUser,
                variables: {
                    input: {
                        id: currentUser.attributes.sub,
                        firstName: firstName,
                        lastName: secondName,
                        description: "description,"
                    },
                },
            });
            console.log(result);
            window.location.href = "/";

        } catch (err) {
            console.log(err);
        }
    };

    if (error) {
        return (
            <div>
                <Navbar />
                <h1>Something Went Wrong! Please Try Again Later.</h1>
            </div>
        );
    }
    return (
        <div className="d-flex flex-column justify-content-center w-100 h-100">
            <Navbar/>
            <h1 className="align-self-center">Edit User Details</h1>
            <Form className="w-50 align-self-center">
                <Form.Group className="mt-2" controlId="firstName">
                    <Form.Label>First Name</Form.Label>
                    <Form.Control
                        type="text"
                        value={firstName}
                        placeholder="Enter Your First Name"
                        onChange={(event) => {
                            setFirstName(event.target.value);
                        }}
                    />
                </Form.Group>
                <Form.Group className="mt-2" controlId="secondName">
                    <Form.Label>Second Name</Form.Label>
                    <Form.Control
                        type="text"
                        value={secondName}
                        placeholder="Enter Your Second Name"
                        onChange={(event) => {
                            setSecondName(event.target.value);
                        }}
                    />
                </Form.Group>
                <Form.Group className="mt-2" controlId="description">
                    <Form.Label>Description</Form.Label>
                    <Form.Control
                        as="textarea"
                        value={description}
                        rows={5}
                        placeholder="Enter Your Description"
                        onChange={(event) => {
                            setDescription(event.target.value);
                        }}
                    />
                </Form.Group>
                <button
                    type="submit"
                    onClick={submitHandler}
                    className="btn btn-primary"
                >
                    Submit
                </button>
            </Form>
        </div>
    );
};
export default EditUser;
Enter fullscreen mode Exit fullscreen mode

It’s just some simple logic. If the user is in edit mode, we will have to trigger, GraphQL mutation ‘updateUser’. Otherwise, we will have to trigger, the GraphQL mutation ‘createUser’. Try submitting your details and come back to Edit Page again. You should see your values.


EditUser Page Showing Previously Entered Details

Mine works, hope yours too.😃

Now, by far we build a pretty cool application. How about letting users choose a profile image? We will need an Amazon S3 Bucket for that.


Adding an S3 Storage

You are into the third and final part of this tutorial.

Through our app, the users can edit their details. Now, I want to add a profile image as well. We will need an S3 Bucket for that. Working with S3 really gets easier with Amplify. Let’s start.

Executing 'amplify add storage'

Run,

amplify add storage
Enter fullscreen mode Exit fullscreen mode

to create a new S3 Bucket. Accept the defaults in the prompt.


Running amplify add storage

Run amplify push to deploy your changes.

Updating the Form

Let’s add image uploading and previewing options into our form. I build a fancy ImageUploader component with an image preview. Make sure to add that under the components directory.

Directory: components/ImageUploader.js

import React from "react";

const ImageUploader = ({imageUploadHandler, image}) => {
    return (
        <div>
            <input className="btn-primary" type="file" onChange={imageUploadHandler}/>
            <div className="image-container">
                {image && <img className="uploaded-image" alt='Uploaded Image' src={URL.createObjectURL(image)}/>}
            </div>
        </div>
);
}

export default ImageUploader;
Enter fullscreen mode Exit fullscreen mode

Add these CSS styles into your styles/global.css file.

amplify-s3-image {
  --height: 300px;
  --width: 300px;
}
.image-container {
  box-shadow: -7px 20px 41px 0 rgba(0,0,0,0.41);
  width: 300px;
  height: 300px;
  max-width: 400px;
  max-height: 400px;
  position: relative;
  display: inline-block;
  overflow: hidden;
  margin: 0;
}
.uploaded-image {
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  height: 100%;
  width: 100%;
  transform: translate(-50%, -50%);
}
Enter fullscreen mode Exit fullscreen mode

We will use AmplifyS3Image Component to preview already uploaded profile images. In order to use these two components, we will make these changes in our EditUser.js file.

We will add these two components inside our form.

<Form className="w-50 align-self-center">

  {editImage && (
    <ImageUploader
      imageUploadHandler={imageUploadHandler}
      image={userImage}
    />
  )}
  {!editImage && (
    <div>
      <button
        type="button"
        className="btn m-2 btn-outline-primary"
        onClick={() => {
          setEditImage(true);
        }}
      >
        Edit Image
      </button>
      <AmplifyS3Image imgKey={user.image} />
    </div>
  )}
Enter fullscreen mode Exit fullscreen mode

AmplifyS3Image component is perfect for displaying S3 images. If you provide the relevant image key (S3 File Name), it displays your S3 Image. Declare these two state variables along with the new imageUploadHandler method.

const [editImage, setEditImage] = useState(!user.image);
const [userImage, setUserImage] = useState(null);

const imageUploadHandler = (event) => {
  setUserImage(event.target.files[0]);
};
Enter fullscreen mode Exit fullscreen mode

Don’t forget to add these imports as well.

import { Storage } from "@aws-amplify/storage";
import { AmplifyS3Image } from "@aws-amplify/ui-react";
import { v4 as uuid } from "uuid";
import ImageUploader from "../components/ImageUploader";
Enter fullscreen mode Exit fullscreen mode

You should see something like this.


Form with ImageUploader

Updating the 'submitHandler' Function

Now let’s update the submitHandler method. Replace the current code with the following.

const submitHandler = async (event) => {
    event.preventDefault();
    const currentUser = await Auth.currentAuthenticatedUser();
    try {
        let key = null;
        if(userImage) {
            key = `${uuid()}${user.firstName}`;

            if(user.image) {
                await Storage.remove(user.image);
            }
            await Storage.put(key, userImage, {
                contentType: userImage.type,
            });
        }
        const result = await API.graphql({
            query: mode === 'EDIT' ? updateUser : createUser,
            variables: {
                input: {
                    id: currentUser.attributes.sub,
                    image: userImage ? key : user.image,
                    firstName: firstName,
                    lastName: secondName,
                    description: description,
                },
            },
        });
        console.log(result);
        window.location.href = "/";

    } catch (err) {
        console.log(err);
    }
};
Enter fullscreen mode Exit fullscreen mode

We can upload an S3 Image into our bucket using Storage.put method, provided by AWS Amplify Library. As I’ve mentioned earlier, we need our file name (image key in S3 ) to access our file again. So we will store that in our database.

If the user is replacing the profile image. We need to remove the existing one. We can remove an S3 file by using Storage.remove method.

Try uploading a new image. Submit the form. Wait until the image uploads. When you navigate back to EditUser Page, you should see your profile image like this.


Uploaded Profile Image Preview

Congratulations on completing the tutorial! 🎉

I think now, you have a good understanding of how to work with Amplify in your Next.js Application. After all, we did work with AppSync API, Cognito Authentication, and AWS S3.

I hope you have completed all the steps without running into any issues. However, if you do, you can ask me from the comments section below.

Video Walkthrough related to this BlogPost:

Part 1: Setting Up the Project and Adding Authentication

Part 2: Adding AppSync API and S3 Storage

Discussion (2)

pic
Editor guide
Collapse
jpmti2016 profile image
Yampier Medina

Great article. What about deployment?

Collapse
kevinmoreland profile image
Kevin Moreland

Very helpful for my senior project, thank you! I used my Cognito user pool for authentication to access the GraphQL instead of an API key.