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
- Introduction
- Setting Up the Project
- Cognito Authentication
- AppSync API
- S3 Storage
Introduction
What We Will Build?
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
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.
- Node.js v10.x or later
- npm v5.x or later
- Amplify CLI (v4.39.0 What I'm using in the Tutorial)
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
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
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.
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
Then you will be prompted for the following information regarding the project you will initialize.
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 });
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.
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
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;
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.
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
Also, add this import in your pages/_app.js
file.
import 'bootstrap/dist/css/bootstrap.min.css';
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;
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);
}
};
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>
);
}
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
Accept the default configurations.
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
}
Save the schema.graphql file. Run amplify push
to push your changes into Amplify Backend.
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.
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;
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.
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);
}
};
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,
},
};
}
}
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 },
});
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;
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.
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
to create a new S3 Bucket. Accept the defaults in the prompt.
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;
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%);
}
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>
)}
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]);
};
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";
You should see something like this.
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);
}
};
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.
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
Top comments (6)
Thank you for this. I just have one question, when I add the getServerSideProps methods and try to upload my changes to Amplify I get the same issue always:
Error occurred prerendering page "/dashboard". Read more: nextjs.org/docs/messages/prerender...
Error: Error for page /dashboard: pages with
getServerSideProps
can not be exported. See more info here: nextjs.org/docs/messages/gssp-exportThis happens on build when the command next build is run. Any clues?
Thank you for this. FYI my updateUser mutation was not working. After hours of trying to debug I found I had conflict detection turned on which seems to be what was causing issue so I ran amplify update api and went into additional settings and said No to conflict detection and that solved issue. Still not clear on what conflict detection does. :) Thanks!
Sorry for barging in here, and it might be too late, but a conflict detection is used for DataStore if you want your application to work offline as well. So, when your app gets back online again, it synchronizes the data, and resolves potential conflicts.
Reference: docs.amplify.aws/lib/datastore/con...
Great article. What about deployment?
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.
how can one display the profile image using next-image?