Joe Gilmore

20 min read

Amplify Studio and NextJS

Guide of how to build a NextJS website and connect it up to an AWS Amplify Studio backend using services such as User Management, File Storage and Content Modelling

Amplify Studio and NextJS

AWS Amplify Studio and NextJS

This article will guide you through the steps required to link up your NextJS front end website to an AWS Amplify Studios backend.

Warning - I am going to keep the front end extremely basic with zero frills for the sake of this tutorial, so the UI itself won't be very interactive or pretty at all... that part is entirely up to you as a developer and/or designer.


Amplify has 2 main parts the first being Hosting for your frontend, and the second being Studio which is the backend. I have already written a guide about how you can host your NextJS website with Amplify so assuming you are now a master at that part lets show how we can add the Amplify Studio backend into a bare bones NextJS Website.

Please note I'm currently using Next V12.0.8 due to a bug with Amplify at the time of writing because the NextJS /api/ doesn't work with the latest version.

  1. First lets create a new NextJS App: yarn create next-app@12.0.8 --typescript
  2. Now in our Amplify App lets click on "Backend environments" and then "Get started"
Deploy Studio
  1. Your Amplify Studio will be created in the Background:
Deploying Studio
  1. Once finished click on "Launch Studio" - this opens up our Amplify Admin Interface to administer our backend:
Studio Created

๐Ÿ‘†๐Ÿผ Please note - this is how we will access our Studio Admin portal, so either note the URL it takes you to or remember this step. This Admin portal is unique to your Apps backend, and you can provide users access to it, this is especially great for allowing colleagues/clients access to a backend without the security worry of creating them a separate ISM account, and save you building your own Admin interface as well! Very cool indeed!

Right, now we have our Studio enabled we are going to do 3 things:

  1. We will setup User Management - this effectively uses Cognito in the background
  2. We will also add File Storage - this effectively uses S3 in the background
  3. We will create a Content Model - this uses AppSync and DynamoDB in the background.

๐Ÿค“ If you are an advanced AWS user then you've probably setup DynamoDB, Cognito, S3 Buckets, Appsync etc yourself so this is effectively a layer on top controlling those services, but if you have no experience with these then not to worry these will all be done in the background for you.

User Management:

For this example we are going to use the absolute default settings, so click "User management" on the left menu and without editing anything lets just press deploy:

Studio User Management

...it will start deploying for you

Studio User Management being deployed

Once finished, it will have created everything we need to allow our front end site to fully authenticate users with registration, forgot password, login etc. So at the top right you will either say something like "Deployment Successful" or show "Local Setup Instructions" and give you a command that you can copy into your command line:

Studio User Management Local Setup Instructions

Copy this and enter it into the command line for your NextJS Website.

  • It will first open up a browser window and authenticate with Studio - click Yes and then return to the CLI (this is so only we can use the command!)
  • Follow the prompts to complete you setup in my case I selected
    • Choose your default editor: Visual Studio Code
    • Choose the type of app that you're building: JavaScript
    • What javascript framework are you using: React
    • Source Directory Path: .
    • Distribution Directory Path: .next
    • Build Command: npm run-script build
    • Start Command: npm run-script start
    • Do you plan on modifying this backend? Yes

Now our front end will add a file called aws-exports.js and a new directory/folder called amplify and we are ready to start using the backend services in our front end.

Please note: in the following examples I am writing code that you would never put into production as there is no user interaction with forms etc and I'm hard coding email address and passwords directly, this is simply to make this tutorial easier to follow, you would need to expand on these examples for your production site

First install the 'aws-amplify' module using npm i aws-amplify

Registration Page

Now lets create a new NextJS file called register.tsx, and add these contents:

import type { NextPage } from "next";
import { useEffect, useState } from "react";
import config from '../aws-exports'
import Amplify, { Auth } from 'aws-amplify'
Amplify.configure({
    ...config,
    ssr: true,
})

const fakeEmail = 'fake@example.com';
const fakePassword = 'FakePassword123!!';

const Register: NextPage = () => {
    const [output, setOutput ] = useState('')
    const onFormSubmit = async () => {
        try{
            const result = await Auth.signUp({ username: fakeEmail, password : fakePassword})
            console.log(result)
            if( result ){
                setOutput(`User signed up OK - a verification code will have been sent via ${result.codeDeliveryDetails.AttributeName} to ${result.codeDeliveryDetails.Destination}`)
            }
        }catch(err : any){ 
            console.error(err)
            setOutput(err.message)
        }
    }

    // This would be done via a interactive form in production... do not do this in the real world!
    useEffect( () => { onFormSubmit() },[])

    return <p>{output}</p>;
};

export default Register;

Change the fakeEmail and fakePassword to ones of your own - it will need to be an email address you actually have as a verification code will be sent there.

Now if you run your NextJS site and visit the page at localhost:3000/register it should register the user and the message should say:

User signed up OK - a verification code will have been sent via email to f***@e***.com

Verify Code Page

So next check your email inbox, and make a note of the 6 digit code that the backend system will have emailed you.

Next add the following code to a page called verify-code.tsx:

import type { NextPage } from "next";
import { useEffect, useState } from "react";
import config from '../aws-exports'
import Amplify, { Auth } from 'aws-amplify'
Amplify.configure({
    ...config,
    ssr: true,
})

const fakeEmail = 'fake@example.com';
const fakeCode = '123456';

const VerifyCode: NextPage = () => {
    const [output, setOutput ] = useState('')
    const onFormSubmit = async () => {
        try{
            const result = await Auth.confirmSignUp( fakeEmail, fakeCode )
            setOutput('Code was confirmed!')
        }catch(err : any){ 
            console.error(err)
            setOutput(err.message)
        }
    }

    // This would be done via a interactive form in production... do not do this in the real world!
    useEffect( () => { onFormSubmit() },[])

    return <p>{output}</p>;
};

export default VerifyCode;

Change fakeEmail to be your email, and set the fakeCode to be the code that was emailed over to you. Now when you run this page by visiting localhost:3000/verify-code it should output "Code was confirmed!"

Login Page:

So far so good, we have registered our user account and verified our email is real by entering the code, then next step is to log the user in, so our final page is going to be called login.tsx and it's contents are as follows:

import type { NextPage } from "next";
import { useEffect, useState } from "react";
import config from '../aws-exports'
import Amplify, { Auth } from 'aws-amplify'
Amplify.configure({
    ...config,
    ssr: true,
})

const fakeEmail = 'fake@example.com';
const fakePassword = 'FakePassword123!!';

const Login: NextPage = () => {
    const [output, setOutput ] = useState('Loading...')
    const onFormSubmit = async () => {
        try{
            const result = await Auth.signIn({ username: fakeEmail, password : fakePassword })
            setOutput('Great - you are now logged in!')
        }catch(err : any){
            console.log(err)
            setOutput(err.message)
        }
    }
    // This would be done via a interactive form in production... do not do this in the real world!
    useEffect( () => { onFormSubmit() },[])

  return <p>{output}</p>;
};

export default Login;

Again... change the fakeEmail and fakePassword to the same ones we registered, and run the page at localhost:3000/login and hey presto! We should be logged in! ๐Ÿ˜Ž

If we now visit our Amplify Studio user management page, we should see our freshly created user:

Studio User Listed

Adding the rest of the Auth parts

OK, so we have Register, Login, and Verify Code parts, but what about the rest, well if you take a look at the Amplify Docs this will give you all the information you need, and will show you how you can invoke these other functions:

  • Auth.resendConfirmationCode()
  • Auth.signOut()
  • Social sign-in (OAuth) for Amazon, Apple, Facebook, Twitter etc
  • Adding MFA with Auth.setupTOTP() & Auth.verifyTotpToken()
  • Auth.forgotPassword() & Auth.forgotPasswordSubmit()
  • Auth Events
  • Deleting users

Next up: Lets add File Storage

Right then... now that we have our User registered and logged in now is the perfect time to add in File Storage so they can upload an image, video or literally any file they like. However, In this example I'll be sticking to just image uploads

So to start, open up your Studio Admin portal again, and in the left hand menu click Storage, and check the boxes for Upload, View and Delete under "Signed-in Users" and then press "Create bucket"

Studio Adding Storage

...it will then deploy:

Studio Deploying Storage

Once it's finished we can click the "Local Setup Instructions" link on the top right again and get copy the amplify pull command and paste it into our NextJS terminal, once finished it'll inform you we now have Storage available:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Category โ”‚ Resource name                             โ”‚ Operation โ”‚ Provider plugin   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Auth     โ”‚ yourprojectname                           โ”‚ No Change โ”‚ awscloudformation โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Storage  โ”‚ s3yourprojectnamestorage95a8f38e          โ”‚ No Change โ”‚ awscloudformation โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Now that this is ready, lets create a new NextJS page called image-upload.tsx to test an image upload and then view the image once it's uploaded.

This page lets the user select an image from their computer and it will upload it to their protected bucket and give them a signedURL to view the image afterwards.

import type { NextPage } from "next";
import { useEffect, useState } from "react";
import config from '../aws-exports'
import Amplify, { Auth, Storage } from 'aws-amplify'
Amplify.configure({
    ...config,
    ssr: true,
})

const ImageUpload: NextPage = () => {

    const [ userAuth, setUserAuth ]  : any = useState(null) 
    useEffect( () => {
        isAuthed()
    }, [])

    async function isAuthed(){
        try{
            await Auth.currentAuthenticatedUser()
            setUserAuth(true)
        }catch(err){
            setUserAuth(false)
        }
    }

    const [ progress, setProgress ]  : any = useState(null) 
    const [ signedImage, setSignedImage ] : any = useState( null )
    const handleFileInputChange = async ( e : any ) => {
        setSignedImage(null)
        const file = e.target.files[0];
        const imageName = Date.now() + '-' + file.name
        const uploadResult = await Storage.put(imageName, file, {
            level: 'protected',
            contentType: file.type,
            progressCallback(progress) {
                setProgress(`Uploading ${Math.round(progress.loaded/progress.total * 100)}%`);
            },
        });
        const signedURL = await Storage.get(uploadResult.key, {level: 'protected'});
        setSignedImage(signedURL)
        setProgress(null)
    }
    return (
        userAuth === null ? <p>Loading...</p> : 
        userAuth === true ?
        <>
            <label htmlFor="file" >
                <div>Select an Image </div>
                <input id="file" name="file" type="file"  onChange={ handleFileInputChange }  accept="image/jpeg,image/gif,image/webp,image/png"/>
            </label>
            { progress && <p>{progress}</p> }
            { signedImage &&  <div className="max-w-lg shadow-lg m-5">
                <img src={signedImage}  alt='Uploaded image' />
            </div>
            }
        </> 
        : <p>You are not logged in!</p>
    )
};

export default ImageUpload;

There we go we have Amplify able to let our users upload images!


Next up: Lets add a Content Model

OK, going back to our Amplify Studio, click on "Data" in the left menu, then "+ Add Model" and lets create a really simple ToDoList with just "id" and "task" as it's columns:

Studio Deploying Storage

Now Click "Save and Deploy" wait for the deployment, and then run the amplify pull command again. You are now ready to start using your Content Model locally.

So lets create a new file called to-do-list.tsx with the following content:

import type { NextPage } from "next";
import { useEffect, useState } from "react";
import config from '../aws-exports'
import Amplify, { Auth } from 'aws-amplify'
Amplify.configure({
    ...config,
    ssr: true,
})

import { DataStore } from '@aws-amplify/datastore';
import { ToDoList } from '../models';

const ToDoListPage: NextPage = () => {

    const [ userAuth, setUserAuth ] : any = useState(null) 
    const [ userEmail, setUserEmail ] : any = useState(null) 
    const [ items, setItems ] : any = useState([]) 

    async function isAuthed(){
        try{
            const resp = await Auth.currentAuthenticatedUser()
            setUserEmail(resp.signInUserSession.idToken.payload.email)
            setUserAuth(true)
        }catch(err){
            setUserAuth(false)
        }
    }

    const addToDoItem = async () => {
        await DataStore.save(
            new ToDoList({
                "task": `${userEmail} - ${new Date()} - ${navigator.userAgent}` //Just users email, date string and useragent to test with
            })
        );
    }

    const getItems = async () => {
        try{
            const dataFromAmplify = await DataStore.query(ToDoList);
            setItems(dataFromAmplify)
        }catch(err){
            console.error( err )
        }
    }
    const deleteItem = async ( id : string ) => {
        try{
            const toDelete : any = await DataStore.query(ToDoList, id);
            DataStore.delete(toDelete);
        }catch(err){
            console.error( err )
        }
    }

    // Observe data in realtime for changes (do not use await on this since it is a long running task and you should make it non-blocking)
    useEffect( () => {
        isAuthed()
        getItems()
        DataStore.observe( ToDoList ).subscribe( () => {
            getItems()
        })
    },[]);
    
    return (
        userAuth === null ? <p>Loading...</p> : 
        userAuth === true ?
        <>
            <h3>To Do List</h3>
            <button onClick={addToDoItem} className="py-2 px-4 border rounded-lg m-2 bg-slate-300">+ Add To Do Item</button>
            <div>
                <ul className="max-w-4xl">
                    {items.map( (item : any) => {
                        return <li key={item.id} className={'border p-3 m-1 text-xs'}>
                            ITEM: {item.task} <button className="text-right float-right" onClick={(e : any) => {deleteItem(item.id)}}>๐Ÿ—‘</button>
                        </li>
                    })
                    }
                </ul>
            </div>
        </> 
        : <p>You are not logged in!</p>
    )
};

export default ToDoListPage;

Now when we visit localhost:3000/to-do-list we should have a button we can press to add an item (it'll populate with the users email, a date string and their useragent for demonstration sake) and we should see it appear in realtime thanks to the DataStore.observe() we are running on our ToDoList model!

We can press the ๐Ÿ—‘ delete icon to easily delete any rows we add!

Studio User Listed

Next Up: Securing the items per user.

So one thing I love about Amplify Studio is how quickly it allows you to get a site with a database up and running. The one thing I absolutely hate about it, is how complicated it is to get the permissions correct. So in this next part of this article I'll explain how...

TO BE
CONTINUED...