This hands-on lab will build a full-stack serverless application on AWS using Amplify, Next.js.
This application will be similar to like-it-or-not for dogs. It will show pictures of dogs and users will either like it or pass it. Those events will then be sent to AWS Kinesis DataStream.
Data ingested into Kinesis DataStream can further be processed for various data related works (e.g. analytics, AI/ML) Think of this application as a gateway for data analytics on AWS.
Once users follow this guide, they will have a working application running on AWS.
We will create a new project using Create Next App.
We will then use Amplify CLI to set up AWS Cloud environment and use Amplify JS Libraries to connect our Next.js app to our back-end on AWS Cloud
This project will be a fully-serverless application with following architecture.
This hands-on lab is expected to be done in 1 to 2 hours
This guide has been made for front-end and back-end developers who want to learn more about building a data-driven full-stack serverless application on AWS.
Having knowledge in React is helpful, but not necessary.
- Next.js application
- Web application Hosting
- Authentication
- Data Inestion into Kinesis DataStream
- Deleting the resources
- Application hosting
- Authentication : Sign Up, Login, Signout
- Sending data to Kinesis DataStream
- Application UI
Before we start, please install
- Node.js v10.x or later
- npm v5.x or later
- git v2.14.1 or later
On a terminal, we will run Amplify CLI to create a infrastructure, start Next.js application on a local machine, and test application on a browser.
If you don't have an AWS account and would like to create and activate an AWS account, please refer to the following link.
Let's create a new project using Create Next App
$ npx create-next-app amplify-love-dogs
move into the amplify-love-dogs
directory and install required packages.
$ cd amplify-love-dogs
$ yarn add aws-amplify @aws-amplify/ui-react lodash
We will use TailwindCSS to style application.
Let's install TailwindCSS related packages in devDependencies.
$ yarn add --dev tailwindcss@latest postcss@latest autoprefixer@latest @tailwindcss/forms
To create Tailwind config files (tailwind.config.js
postcss.config.js
), let's run the following.
$ npx tailwindcss init -p
Now, let's update tailwind.config.js
as following.
This is to do tree-shake unused styling in production build
// tailwind.config.js
module.exports = {
- purge: [],
+ purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
- plugins: [],
+. plugins: [require('@tailwindcss/forms')],
}
To use Tailwind's base, component, and utilities style, Let's update ./styles/globals.css
/* ./styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
If you would like to know more about installing TailwindCSS, plesae check here
Let's update pages/index.js, which renders / root page.
/* pages/index.js */
import Head from "next/head";
function Home() {
return (
<div>
<Head>
<title>Amplify Love Dogs</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🐕</text></svg>"
/>
</Head>
<div className="container mx-auto">
<main className="bg-white">
<div className="px-4 py-16 mx-auto max-w-7xl sm:py-24 sm:px-6 lg:px-8">
<div className="text-center">
<p className="mt-1 text-4xl font-extrabold text-gray-900 sm:text-5xl sm:tracking-tight lg:text-6xl">
Amplify Love Dogs
</p>
<p className="max-w-xl mx-auto mt-5 text-xl text-gray-500">
Welcome to Amplify Love Dogs
</p>
</div>
</div>
</main>
</div>
<footer></footer>
</div>
);
}
export default Home;
Let's run yarn dev
to start a local server, and check if the page
loads with no issues on a browser at localhost:3000
$ yarn dev
Let's create a git repository for this project at (https://github.com/new)
Once you create a repository, let's initialize a git in your folder, and add the created repository url.
$ git init
$ git remote add origin [email protected]:username/project-name.git
$ git add .
$ git commit -m 'initial commit'
$ git push origin main
Let's install Amplify CLI
$ npm install -g @aws-amplify/cli
Now, let's configure CLI to use your AWS credential.
If you would like to know more about the steps to create a credential, please check this video here
$ amplify configure
- Specify the AWS Region: ap-northeast-2
- Specify the username of the new IAM user: amplify-cli-user
> In the AWS Console, click Next: Permissions, Next: Tags, Next: Review, & Create User to create the new IAM user. Then return to the command line & press Enter.
- Enter the access key of the newly created user:
? accessKeyId: (<YOUR_ACCESS_KEY_ID>)
? secretAccessKey: (<YOUR_SECRET_ACCESS_KEY>)
- Profile Name: amplify-cli-user
Let's initialze your Amplify project.
$ amplify init
- Enter a name for the project: amplifyforum
- Enter a name for the environment: dev
- Choose your default editor: Visual Studio Code (or your default editor)
- Please choose the type of app that youre building: javascript
- What javascript framework are you using: react
- Source Directory Path: src
- Distribution Directory Path: out
- Build Command: npm run-script build
- Start Command: npm run-script start
- Do you want to use an AWS profile? Y
- Please choose the profile you want to use: amplify-cli-user
You must change Distribution Directory Path to
out
. After you build and export your Next.js, build artifacts will be placed inout
directory
Once
amplify init
is done, amplify folder will be created andaws-exports.js
file will be created in src folder.
src/aws-exports.js is where you will find Amplify config infos.
amplify/team-provider-info.json contains variables for Amplify project's back-end environment. If you plan to share the same back-end environment, you should share this file. If not (e.g. opening this project to a public), you should not share this file (e.g. adding this file in
.gitignore
)
For more info, please check (https://docs.amplify.aws/cli/teams/shared)
You can check Amplify project's status with amplify status
command.
$ amplify status
If you want to check with Amplify console,amplify console
should launch a console in your browser.
$ amplify console
Once we have Amplify project ready, we now need to make our Next.js app
to be aware of Amplify project.
We can do this by making the top level component to configure Amplify with src/aws-exports.js
file
Let's open pages/_app.js and add the following.
import '../styles/globals.css'
+ import Amplify from "aws-amplify";
+ import config from "../src/aws-exports";
+ Amplify.configure(config);
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
Once it's done, our Next.app is ready to use AWS managed by Amplify.
Amplify Console takes care of application hosting as well as CI and deployment.
First, let's update package.json as follows.
"scripts": {
"dev": "next dev",
- "build": "next build",
+ "build": "next build && next export",
"start": "next start"
},
next export
generates static HTML from the Next.js app so the application can be served as a static file without the need of a Node server.
As of 2021-04, Amplify hosting can only serve static files. However, server-side rending will soon be supported.
To add hosting, let's run amplify add hosting
$ amplify add hosting
? Select the plugin module to execute: Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type: Manual deployment
To apply the change we just made, let's run amplify push
$ amplify push
To publish/deploy our application, run amplify publish
$ amplify publish
Once deployment is finished, a url will be printed. Go to the url in your browser, and make sure your application loads correctly.
Let's now add authentication.
To add authentication feature, run amplify add auth
$ amplify add auth
? Do you want to use default authentication and security configuration? Default configuration
? How do you want users to be able to sign in when using your Cognito User Pool? Username
? Do you want to configure advanced settings? No, I am done.
To apply the change, run amplify push
$ amplify push
? Are you sure you want to continue? Yes
Using withAuthencator
HOC provided by amplify-ui, we can make sure
pages are protected by authentication.
Once applied, users must log in to access the page. If not, they will be redirected to a login page.
This UX flow is all taken care of by withAuthenticator
To test, let's update /pages/index.js
/* pages/index.js */
import Head from "next/head";
+ import { withAuthenticator } from "@aws-amplify/ui-react";
- export default Home;
+ export default withAuthenticator(Home);
Authenticator UI Component document here
Let's start a dev server and test in the browser.
yarn dev
If you try to load a root / page, you will be redirected to a login.
Let's create a new account with sign-up.
Once signed up, you will receive a confirmation code in your email.
Entering the confirmation code will complete the new user sign up.
You can check newly created users in Auth console
$ amplify console auth
> Choose User Pool
Let's add signout by using Signout UI component.
Add AmplifySignout
compoent somewhere in your page component. (e.g.
pages/index.js)
import { withAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
/* UI 어딘가에 넣어주세요. */
<AmplifySignOut />;
Sign Out UI Component doc here
Let's click on signout button, and make sure you can logout successfully.
When logged in, you can access authenticated user's information with Auth.currentAuthenticatedUser()
Let's update pages/index.js to print user information in console.
+ import { useEffect } from "react";
+ import { Auth } from "aws-amplify";
function Home() {
+ useEffect(() => {
+ checkUser(); // new function call
+ }, []);
+
+ async function checkUser() {
+ const user = await Auth.currentAuthenticatedUser();
+ console.log("user: ", user);
+ console.log("user attributes: ", user.attributes);
+ }
/* 이전과 동일 */
}
Once you load the page with browser console opened, you will see the authenticated user's information and attributes in the console.
We need to fetch data. We will create files that contain (1) breed names (2) images for breeds
Create a file in src/breeds.js and fill with following
const BREEDS = {
affenpinscher: [],
african: [],
airedale: [],
akita: [],
appenzeller: [],
australian: ["shepherd"],
basenji: [],
beagle: [],
bluetick: [],
borzoi: [],
bouvier: [],
boxer: [],
brabancon: [],
briard: [],
buhund: ["norwegian"],
bulldog: ["boston", "english", "french"],
bullterrier: ["staffordshire"],
cairn: [],
cattledog: ["australian"],
chihuahua: [],
chow: [],
clumber: [],
cockapoo: [],
collie: ["border"],
coonhound: [],
corgi: ["cardigan"],
cotondetulear: [],
dachshund: [],
dalmatian: [],
dane: ["great"],
deerhound: ["scottish"],
dhole: [],
dingo: [],
doberman: [],
elkhound: ["norwegian"],
entlebucher: [],
eskimo: [],
finnish: ["lapphund"],
frise: ["bichon"],
germanshepherd: [],
greyhound: ["italian"],
groenendael: [],
havanese: [],
hound: ["afghan", "basset", "blood", "english", "ibizan", "plott", "walker"],
husky: [],
keeshond: [],
kelpie: [],
komondor: [],
kuvasz: [],
labradoodle: [],
labrador: [],
leonberg: [],
lhasa: [],
malamute: [],
malinois: [],
maltese: [],
mastiff: ["bull", "english", "tibetan"],
mexicanhairless: [],
mix: [],
mountain: ["bernese", "swiss"],
newfoundland: [],
otterhound: [],
ovcharka: ["caucasian"],
papillon: [],
pekinese: [],
pembroke: [],
pinscher: ["miniature"],
pitbull: [],
pointer: ["german", "germanlonghair"],
pomeranian: [],
poodle: ["miniature", "standard", "toy"],
pug: [],
puggle: [],
pyrenees: [],
redbone: [],
retriever: ["chesapeake", "curly", "flatcoated", "golden"],
ridgeback: ["rhodesian"],
rottweiler: [],
saluki: [],
samoyed: [],
schipperke: [],
schnauzer: ["giant", "miniature"],
setter: ["english", "gordon", "irish"],
sheepdog: ["english", "shetland"],
shiba: [],
shihtzu: [],
spaniel: [
"blenheim",
"brittany",
"cocker",
"irish",
"japanese",
"sussex",
"welsh",
],
springer: ["english"],
stbernard: [],
terrier: [
"american",
"australian",
"bedlington",
"border",
"dandie",
"fox",
"irish",
"kerryblue",
"lakeland",
"norfolk",
"norwich",
"patterdale",
"russell",
"scottish",
"sealyham",
"silky",
"tibetan",
"toy",
"westhighland",
"wheaten",
"yorkshire",
],
vizsla: [],
waterdog: ["spanish"],
weimaraner: [],
whippet: [],
wolfhound: ["irish"],
};
export default BREEDS;
Download this file as breed-image-url.json
Let's install additional packages for UI in devDependencies.
$ yarn add --dev @headlessui/react @heroicons/react
Let's update pages/index.js as follows.
import Head from "next/head";
import { useEffect, useState } from "react";
import _ from "lodash";
import BREEDS from "../src/breeds";
import fs from "fs";
import {
HeartIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "@heroicons/react/outline";
import { withAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
import { Auth } from "aws-amplify";
import { Analytics } from "aws-amplify";
async function fetchBreeds() {
return [...Object.keys(BREEDS)];
}
async function fetchBreedImageUrls(mainBreed) {
const data = fs.readFileSync("breed-image-url.json");
const imageUrls = JSON.parse(data);
return imageUrls;
}
export async function getStaticProps(context) {
const breeds = await fetchBreeds();
const breedImageUrls = await fetchBreedImageUrls();
return {
props: { initialBreeds: breeds, initialBreedsUrls: breedImageUrls },
};
}
function DogCard({ imageUrl, onNext, onPrev, onLike }) {
return (
<>
<div className="max-w-screen-lg mx-auto h-80">
<img
src={imageUrl}
alt=""
className="object-cover h-full mx-auto pointer-events-none group-hover:opacity-75"
/>
</div>
<div className="mt-4">
<span className="relative z-0 inline-flex rounded-md shadow-sm">
<button
onClick={onPrev}
type="button"
className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
>
<ChevronLeftIcon className="w-5 h-5" aria-hidden="true" />
</button>
<button
onClick={onLike}
type="button"
className="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
>
<HeartIcon className="w-5 h-5" aria-hidden="true" />
</button>
<button
onClick={onNext}
type="button"
className="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
>
<ChevronRightIcon className="w-5 h-5" aria-hidden="true" />
</button>
</span>
</div>
</>
);
}
function Home({ initialBreeds, initialBreedsUrls }) {
const [breeds, setBreeds] = useState(initialBreeds);
const [breedsUrls, setBreedsUrls] = useState(initialBreedsUrls);
const [randomBreed, setRandomBreed] = useState();
const [randomBreedUrl, setRandomBreedUrl] = useState();
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
console.log("breeds = ", breeds);
checkUser();
}, []);
async function checkUser() {
const user = await Auth.currentAuthenticatedUser();
console.log("user: ", user);
console.log("user attributes: ", user.attributes);
setCurrentUser(user);
}
useEffect(() => {
setRandomBreed(_.sample(breeds));
}, [breeds]);
useEffect(() => {
console.log("breed = ", randomBreed);
const urls = breedsUrls[randomBreed];
setRandomBreedUrl(_.sample(urls));
}, [randomBreed]);
const handleLike = () => {
setRandomBreed(_.sample(breeds));
};
const handlePrev = () => {
setRandomBreed(_.sample(breeds));
};
const handleNext = () => {
setRandomBreed(_.sample(breeds));
};
return (
<div>
<Head>
<title>Amplify Love Dogs</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🐕</text></svg>"
/>
</Head>
<div className="container mx-auto">
<main className="bg-white">
<div className="px-4 py-16 mx-auto max-w-7xl sm:py-24 sm:px-6 lg:px-8">
<div className="text-center">
<p className="mt-1 text-4xl font-extrabold text-gray-900 sm:text-5xl sm:tracking-tight lg:text-6xl">
Amplify Love Dogs
</p>
<p className="max-w-xl mx-auto mt-5 text-xl text-gray-500">
Welcome to Amplify Love Dogs
</p>
<div className="mt-4">
{randomBreed && randomBreedUrl && (
<DogCard
breed={randomBreed}
imageUrl={randomBreedUrl}
onLike={handleLike}
onPrev={handlePrev}
onNext={handleNext}
/>
)}
</div>
</div>
<div className="mt-6">
<AmplifySignOut />
</div>
</div>
</main>
</div>
<footer></footer>
</div>
);
}
export default withAuthenticator(Home);
Let's create data endpoint where application will send data to. In this guide, we will set up a Kinesis DataStream.
Let's create a Kinesis DataStream endpoint with Amplify cli
$ amplify add analytics
? Select an Analytics provider : Amazon Kinesis Streams
? Enter a Stream name : amplifylovedogsKinesis
? Enter number of shards : 1
? Apps need authorization to send analytics events. Do you want to allow guests and unauthenticated users to send analytics events? (we recommend you allow this when getting started) : Yes
To apply the change we just made, run amplify push --y
$ amplify push --y
Update _app.js file.
import config from "../src/aws-exports";
+ import { Analytics, AWSKinesisProvider } from "aws-amplify";
Amplify.configure(config);
+ Analytics.addPluggable(new AWSKinesisProvider());
+ Analytics.configure({
+ AWSKinesis: {
+ region: config.aws_project_region,
+ },
+ });
Let's update pages/index.js to send user events to Kinesis
import { Auth } from "aws-amplify";
+ import { Analytics } from "aws-amplify";
/* same as before */
function Home({ initialBreeds, initialBreedsUrls }) {
/* same as before */
const handleLike = () => {
+ recordUserActivity("like");
setRandomBreed(_.sample(breeds));
};
const handlePrev = () => {
+ recordUserActivity("prev");
setRandomBreed(_.sample(breeds));
};
const handleNext = () => {
+ recordUserActivity("next");
setRandomBreed(_.sample(breeds));
};
+ const recordUserActivity = (action) => {
+ const userActivity = {
+ username: currentUser.username,
+ action,
+ breed: randomBreed,
+ };
+ console.log(userActivity);
+ Analytics.record(
+ {
+ data: { userActivity },
+ streamName: "amplifylovedogsKinesis-dev" // TODO: Set to Kinesis Stream Name, and it has to include environment name too, e.g.: 'traveldealsKinesis-dev'
+ },
+ "AWSKinesis"
+ );
+ };
}
In the browser, do some action. (e.g. like a dog) and see if events get sent to the Kinesis without any issues.
Let's open up Kinesis Console and see if data has been recieved.
Click on Data Streams
and select the stream that we created (e.g. amplifylovedogsKinesis-dev
)
Check Monitoring
tab to see if there are any changes in Incoming data and put records
If you want to remove one of the services you added with Amplify CLI, you can run amplify remove
For example, amplify remove auth
will remove authentication feature.
$ amplify remove auth
$ amplify push
If you are not sure about which services you have enabled, you can check with amplify status
$ amplify status
If you want to delete Amplify project completely, run amplify delete
$ amplify delete