Building Full Stack Apps Fast with AWS Amplify and React

Building modern applications requires both frontend and backend development skills. But what if you could build full stack apps quickly without being an expert in both domains?

Enter AWS Amplify – a set of tools and services that enable developers to quickly build full stack applications on AWS. Amplify provides an easy-to-use interface to configure backend resources, and SDKs to integrate those services into your web or mobile apps.

In this post, we‘ll walk through building a full stack song sharing app using Amplify and React. You‘ll learn how to:

  • Configure a backend with authentication, storage, database, and API
  • Integrate the backend into a React app
  • Allow users to view and play songs, upload new songs, and like their favorites
  • Deploy the app with Amplify hosting

By the end, you‘ll have a fully functional app built with modern serverless technologies. Let‘s rock and roll!

Prerequisites

To follow along, you‘ll need:

  • Basic proficiency with React and JavaScript
  • An AWS account (sign up for free at aws.amazon.com)
  • Node.js and npm installed on your machine

Creating the React App

Let‘s start by scaffolding a new React app with create-react-app:

npx create-react-app my-amplify-app
cd my-amplify-app

Next, we‘ll install the AWS Amplify libraries:

npm install --save aws-amplify @aws-amplify/ui-react

Configuring the Backend with Amplify

Now for the exciting part – creating our backend services. From the root directory of the project, run:

amplify init

This command initializes a new Amplify project. Follow the prompts to give your project a name, environment name, and set the default text editor.

Next, we‘ll add an authentication service to enable user sign-up and login:

amplify add auth

Select the default configuration to set up authentication with Amazon Cognito.

To store information about songs, we need a database. We‘ll use Amazon DynamoDB, a NoSQL database:

amplify add storage

Select NoSQL, provide a table name of "Songs", and add "id" (string) as a partition key.

To store the actual audio files, we‘ll use Amazon S3, a scalable object storage service:

amplify add storage

Choose Content (Images, audio, video, etc), give the resource a name, and select Auth users only under Who should have access.

Finally, let‘s add an API to act as the interface between our frontend and backend services:

amplify add api

Select GraphQL, then give the API a name and choose Amazon Cognito User Pool for the authentication type. Select Single object with fields under Annotation, then edit the schema to define our Song type:

type Song @model @auth(rules: [{ allow: owner }]) {
  id: ID!
  title: String!
  description: String
  filePath: String!
  owner: String
  liked: Boolean
}

This schema defines the fields of our Song object, and specifies that only the owner of a song object should have access to modify it.

With all our resources configured, we can create them in our AWS account by running:

amplify push

Review the resources to be created, then select Yes to create them. Wait a few minutes as Amplify provisions the services.

Integrating the Backend into React

With our backend ready, let‘s integrate it into our React app.

Configure the React app to work with Amplify by adding the following code to src/index.js:

import Amplify from ‘aws-amplify‘;
import awsconfig from ‘./aws-exports‘;

Amplify.configure(awsconfig);

This code imports the Amplify library and the aws-exports file (generated by amplify push), and configures Amplify with our project-specific settings.

To allow users to sign up and log in, add the following code to src/App.js:

import { withAuthenticator } from ‘@aws-amplify/ui-react‘;

function App() {
  return (
    <div className="App">

    </div>
  );
}

export default withAuthenticator(App);

The withAuthenticator higher order component adds complete authentication flows to our app. We‘ll see sign up and login pages automatically when we run the app.

Listing and Playing Songs

To display a list of songs, we‘ll fetch them from our API. Add the following code to src/App.js:

import { useState, useEffect } from ‘react‘;
import { API, graphqlOperation } from ‘aws-amplify‘;
import { listSongs } from ‘./graphql/queries‘;

function App() {
  const [songs, setSongs] = useState([]);

  useEffect(() => {
    fetchSongs();
  }, []);

  async function fetchSongs() {
    try {
      const songData = await API.graphql(graphqlOperation(listSongs));
      setSongs(songData.data.listSongs.items);
    } catch (err) {
      console.log(‘error fetching songs‘);
    }
  }

  return (
    // ... existing code ...

    <div className="song-list">
      {songs.map((song, idx) => {
        return (
          <Paper variant="outlined" elevation={2} key={`song${idx}`}>
            <div className="song-info">
              <IconButton aria-label="play">
                <PlayArrowIcon />
              </IconButton>
              <div>
                <div className="song-title">{song.title}</div>
                <div className="song-description">{song.description}</div>
                <div className="song-owner">{song.owner}</div>
              </div>
            </div>
          </Paper>
        );
      })}
    </div>

    // ... existing code ... 
  );
}

This code uses the listSongs query (generated by Amplify from our GraphQL schema) to fetch all songs. It then renders them as Material UI Paper components.

To play a song when the play button is clicked, we can use the AWS Amplify Storage module to get a pre-signed URL for the song‘s audio file, then play it using the browser‘s Audio API:

import { Storage } from ‘aws-amplify‘;

async function toggleSong(idx) {
  if (songPlaying === idx) {
    songRef.current.pause();
    setSongPlaying(null);
  } else {
    if (songRef.current) {
      songRef.current.pause();
    }

    try {
      const data = await Storage.get(songs[idx].filePath, { level: ‘public‘ });
      songRef.current = new Audio(data);
      await songRef.current.play();
      setSongPlaying(idx);
    } catch (err) {
      console.log(‘error playing song‘, err);
    }
  }
}

Uploading New Songs

To allow users to upload new songs, we can create an upload form with a file selector for the audio file, and input fields for the song‘s metadata:

function App() {
  // ... existing code ...
  const [title, setTitle] = useState(‘‘);
  const [description, setDescription] = useState(‘‘);
  const [file, setFile] = useState();

  async function addSong() {
    try {
      // Upload the file to S3
      const fileName = `${Date.now()}-${file.name}`;
      const stored = await Storage.put(fileName, file, {
        contentType: ‘audio/mp3‘ // contentType is optional
      });

      // Save the song metadata to the database
      const songData = {
        title, 
        description, 
        filePath: stored.key,
        owner: user.username,
        liked: false
      };
      await API.graphql(graphqlOperation(createSong, {input: songData}));

      // Clear the form
      setTitle(‘‘);
      setDescription(‘‘);
      setFile(null);

      // Refresh the song list
      fetchSongs();
    } catch (err) {
      console.log(‘error uploading song:‘, err);
    }
  }

  return (
    // ... existing code ...

    <div className="song-upload">
      <TextField
        label="Title" 
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <TextField
        label="Description"
        value={description}
        onChange={e => setDescription(e.target.value)}
      />
      <input
        type="file"
        accept="audio/mp3"
        onChange={e => setFile(e.target.files[0])}
      />
      <IconButton onClick={addSong}>
        <PublishIcon />
      </IconButton>
    </div>

    // ... existing code ...
  );
}

This code creates a form with input fields bound to state variables. When the upload button is clicked, it:

  1. Uploads the audio file to S3 using the Storage.put method
  2. Saves the song metadata (including the S3 file path) to DynamoDB using the createSong mutation
  3. Clears the form and refreshes the song list

Adding Likes

As a final touch, let‘s allow users to "like" their favorite songs. We can add a like button next to each song:

function Song({song, idx, onLike}) {
  // ... existing code ...

  return (
    <Paper variant="outlined" elevation={2} key={`song${idx}`}>
      <div className="song-info">
        // ... existing code ...
        <IconButton aria-label="like" onClick={() => onLike(idx)}>
          <FavoriteIcon color={song.liked ? "secondary" : "disabled"} />
        </IconButton>
      </div>
    </Paper>
  )
}

function App() {
  // ... existing code ...

  async function toggleLike(idx) {
    try {
      const song = songs[idx];
      song.liked = !song.liked;

      await API.graphql(graphqlOperation(updateSong, {input: song}));

      setSongs([...songs.slice(0, idx), song, ...songs.slice(idx + 1)]);
    } catch (err) {
      console.log(‘error liking song:‘, err);
    }
  }

  return (
    // ... existing code ...

    <div className="song-list">
      {songs.map((song, idx) => {
        return (
          <Song 
            song={song} 
            key={song.id} 
            onLike={toggleLike}
          />
        )
      })}
    </div>

    // ... existing code ...
  );
}

Here we‘ve extracted the song item into its own Song component. When the like button is clicked, it toggles the liked state of the song and updates it in the database using the updateSong mutation.

Deployment

With our app complete, we‘re ready to deploy it! AWS Amplify makes this incredibly easy. Just run:

amplify add hosting

Select the Hosting with Amplify Console option. Amplify will build your app and provide you with a URL where you can access it.

And there you have it! We‘ve built a functional, full stack song sharing app with AWS Amplify and React. Thanks to Amplify, we were able to focus on our application logic instead of getting bogged down in backend configuration or deployments.

Next Steps

While this app is quite comprehensive, there are always more features that could be added. Here are a few ideas:

  • Add user profiles to show a user‘s uploaded songs and liked songs
  • Implement playlists so users can create and share collections of songs
  • Support more audio formats besides MP3
  • Add a search bar to find songs by title or description
  • Customize the style of the app with your own design

I hope this post has given you a taste of what‘s possible with AWS Amplify. Go forth and build incredible things!

Similar Posts