Create a Fullstack Next.js App with Authentication, Data, and Storage

Next.js is one of my favorite tools for web development -- but it's a frontend framework. How can you build a fullstack application with it? In this tutorial, we'll build a Server-side Rendered Next.js app with a database-backed backend, static image storage, and authentication. It'll be National Park themed -- signed in users will be able to create new parks, and everyone will be able to view all added parks.

If you're new to Next.js, I wrote a tutorial on how to get started with it. If you're new to AWS Amplify, here's a tutorial on that as well.

Please note that I work as a Developer Advocate on the AWS Amplify team, if you have any feedback or questions about it, please reach out to me or ask on our discord - discord.gg/amplify!

Create your app Backend

First, create a new Next app:

npx create-next-app national-parks

Make sure you have the Amplify CLI installed, if not follow these instructions!

Then, initialize Amplify for your project. You should be able to accept the configuration it generates for you and then select your AWS profile or enter your access keys.

amplify init

Then we'll configure the needed services. First we'll add authentication.

amplify add auth

Answer the ensuing questions like so:

Do you want to use the default authentication and security configuration? Default configuration
How do you want users to be able to sign in? Username
Do you want to configure advanced settings? No, I am done.

Now we'll add storage to our app. Select the default configuration options for all questions other than who should have access -- there, give authenticated users access to all actions and unauthenticated users the ability to read data.

amplify add storage

? Please select from one of the below mentioned services: Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: s37cd140d1
? Please provide bucket name: nationalparkbrowser248f6fd94d4f46f99a951df475e8
? Who should have access: Auth and guest users
? What kind of access do you want for Authenticated users? create/update, read, delete
? What kind of access do you want for Guest users? read
? Do you want to add a Lambda Trigger for your S3 Bucket? No

Finally we'll create an API. We'll select GraphQL and use an API key for authorization. Open up the GraphQL schema in your text editor.

amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: nationalparks
? Choose the default authorization type for the API API key
? Enter a description for the API key:
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)

If your schema.graphql file didn't open in your text editor, open it up. It will be under amplify/backend/api/nationalparks. There will be a sample GraphQL schema already in there, but we'll edit it to have the data format we need.

We'll create two models: S3Object and Park. Park will store our parks -- each park will have an id, a name, and an image. That image will reference an image stored in Amazon S3 (we created a bucket when we ran amplify add storage). The S3Object will have information about the image stored on S3 -- it's bucket, region, and key. We'll use the key to access the images in our app.

type S3Object {
  bucket: String!
  region: String!
  key: String!
}

type Park @model {
  id: ID!
  name: String!
  image: S3Object
}

Now run amplify push to deploy your resources to the cloud! You now have a fully deployed backend.

Install the Amplify libraries. These will allow us to use JavaScript helper code and React components to expedite our frontend development.

npm i aws-amplify @aws-amplify/ui-react

Once our backend is deployed, we'll need to link our frontend to our backend using Amplify.configure(). We'll use the configuration information in the src/aws-exports.js file that's automatically generated by Amplify and also make sure to set the ssr flag to true so that we can pull from our API on the server.

Add this to the top of your pages/_app.js:

import Amplify from 'aws-amplify'
import config from '../src/aws-exports'

Amplify.configure({ ...config, ssr: true })

Frontend Logic

Phew! Done with configuration code, now we can write our frontend React logic. Let's first create a form in order to create a new park. Create a file pages/create-park.js that will house a page that will render our form. Create a React component in the file.

// create-park.js
function CreatePark () {
  return <h1>Create Park</h1>
}

export default CreatePark

Then, we'll use the withAuthenticator higher-order component to require sign in before going to the /create-park page. It will also enable sign up and require account confirmation.

// create-park.js
import { withAuthenticator } from '@aws-amplify/ui-react'

function CreatePark () {
  return <h1>Create Park</h1>
}

export default withAuthenticator(CreatePark)

Now we'll create a React form where a user can input the name of the park and an image.

// create-park.js
import { useState } from 'react'
import { withAuthenticator } from '@aws-amplify/ui-react'

function CreatePark () {
  const [name, setName] = useState('')
  const [image, setImage] = useState('')

  const handleSubmit = async () => {

  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Create a Park</h2>
      <label htmlFor='name'>Name</label>
      <input type='text' id='name' onChange={e => setName(e.target.value)} />
      <label htmlFor='image'>Image</label>
      <input type='file' id='image' onChange={e => setImage(e.target.files[0])} />
      <input type='submit' value='create' />
    </form>
  )
}

export default withAuthenticator(CreatePark)

Finally, we'll implement the handleSubmit function which will upload the user's image to S3 and then store our newly created park in our data base using our GraphQL API. We'll import the configuration information from the aws-exports.js again and one of the GraphQL mutations that Amplify generated in the src/graphql directory.

Then, we'll upload the image using Storage.put() with the image's name as the key and the image itself as the value. Then, we'll use API.graphql to run the graphQL mutation with the user's inputted data and configuration information about the S3 bucket.

// create-park.js
import { useState } from 'react'
import { API, Storage } from 'aws-amplify'
import { withAuthenticator } from '@aws-amplify/ui-react'

import { createPark } from '../src/graphql/mutations'
import config from '../src/aws-exports'

function CreatePark () {
  const [name, setName] = useState('')
  const [image, setImage] = useState('')

  const handleSubmit = async e => {
    e.preventDefault()
    // upload the image to S3
    const uploadedImage = await Storage.put(image.name, image)
    console.log(uploadedImage)
    // submit the GraphQL query 
    const newPark = await API.graphql({
      query: createPark,
      variables: {
        input: {
          name,
          image: {
            // use the image's region and bucket (from aws-exports) as well as the key from the uploaded image
            region: config.aws_user_files_s3_bucket_region,
            bucket: config.aws_user_files_s3_bucket,
            key: uploadedImage.key
          }
        }
      }
    })
    console.log(newPark)
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Create a Park</h2>
      <label htmlFor='name'>Name</label>
      <input type='text' id='name' onChange={e => setName(e.target.value)} />
      <label htmlFor='image'>Image</label>
      <input type='file' id='image' onChange={e => setImage(e.target.files[0])} />
      <input type='submit' value='create' />
    </form>
  )
}

export default withAuthenticator(CreatePark)

If you want, here are a few lines of CSS you can paste into the styles/globals.css file to make the app look a little more presentable.

amplify-s3-image {
  --width: 70%;
  overflow: hidden;
  margin: 0 auto;
}

.container {
  max-width: 1000px;
  margin: 0 auto;
  padding: 0 2rem;
  text-align: center;
}

.img-square img h2 {
  margin: 0 auto;
  text-align: center;
}

Finally, we'll list all of the parks on the index page. We'll use the listParks graphql query that was generated in src/graphql/queries.js to fetch the parks and the AmplifyS3Image component to render the images on the page. We'll fetch the parks on the server-side so that our app will dynamically update when new parks are added.

import Head from 'next/head'
import { withSSRContext } from 'aws-amplify'
import { listParks } from '../src/graphql/queries'
import { AmplifyS3Image } from '@aws-amplify/ui-react'
import Link from 'next/link'

export async function getServerSideProps () {
  const SSR = withSSRContext()
  const { data } = await SSR.API.graphql({ query: listParks })
  return {
    props: {
      parks: data.listParks.items
    }
  }
}

export default function Home ({ parks }) {
  return (
    <div>
      <Head>
        <title>National Parks</title>
      </Head>
      <div className='container'>
        <h1>National Parks <Link href='/create-park'>(+)</Link></h1>
        <div className='img-grid'>
          {parks.map(park => {
            return (
              <div key={park.id} className='img-square'>
                <h2>{park.name}</h2>
                {/* use the AmplifyS3Image component to render the park's image using its S3 key */}
                <AmplifyS3Image imgKey={park.image.key} height='200px' />
              </div>
            )
          })}
        </div>
      </div>
    </div>
  )
}

Frontend Deployment

Now our app has a complete frontend! Let's deploy it via Amplify hosting. Push your code to a GitHub repository, then open up the Amplify Console for your app. Click on the frontend environments tab and then the connect app button. Choose your repository, use the auto generated configuration, and save and deploy. It'll take a few minutes and then your app will be live!

No extra configuration is needed, Amplify will infer that you're creating a SSR Next.js app and deploy the needed hosting resources for your app. Here's more information if you're interested!

Cleanup

You may not want to keep the app deployed, in which case you can click the delete app button in the AWS console or run amplify delete from your command line. This will de-provision your backend resources from your AWS account!

AWS Amplify allows you to make your Next.js app fullstack with data, image storage, and authentication without having to have heavy knowledge of the cloud or fullstack development.

19