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
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.
yarn create next-app@12.0.8 --typescript
๐๐ผ 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:
๐ค 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.
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:
...it will start deploying for you
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:
Copy this and enter it into the command line for your NextJS Website.
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
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
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!"
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:
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:
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"
...it will then deploy:
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!
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:
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!
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...