How to Create a Serverless File-Management System With React

Responses (0)

Clap
0|0|

Serving files is what web-servers are for. How it works hasn't changed since the dawn of the internet. "No big deal!" you may think. We use a simple link like this:

But modern web-technologies have changed the way we work.

  1. We do not use classic web-servers anymore. We use cloud hosting and Serverless apps.

  2. Our back-end code written in PHP does not render all the data in HTML-templates anymore. We write full-stack applications in JavaScript with Node.js. It is up to us to put the data into the HTML-elements.

  3. Our front-end JavaScript code does not look like the worst hack ever anymore. We use React, JSX, and functional programming techniques to write concise, understandable, and thus maintainable code.

The resulting problem is: We have to "re-solve" some of the problems. But it is not too hard.

In this hands-on tutorial, we're going to build a full-stack app from scratch. It contains a React-based web-app, an AWS Lambda/API-Gateway back-end service, and an AWS S3 file storage.

Did I mention that we will be able to start the whole stack locally with a single command? No? yes, we can!

Oh, did I mention that we will be able to deploy the whole stack to our AWS account with a single command? Again no? I am so forgetful. Of course, we can!

The following image depicts the architecture of our full-stack app.

You'll find the full source code of this project at GitHub. This project is also part of React-Architect: Full-Stack React App Development and Serverless Deployment.

Create, Start, and Deploy Your Full-Stack React-App#

There are manifold ways of creating a React app. Facebook’s create-react-app script is the most famous one. But, while it supports you when you develop your app, it leaves you unsupported when it comes to deploying it.

We use Infrastructure-Components. These React-Components let us define our infrastructure architecture as part of our React-app. We don’t need any other configuration like Webpack, Babel, or Serverless anymore.

You can set up your project in three ways:

  1. Download your customized boilerplate code from www.infrastructure-components.com

  2. Clone this GitHub-repository (template project)

  3. Install the libraries manually, see this post for more details

You’ll get the following file structure:

The package.json specifies all the dependencies of your project. In this tutorial, we use the following libraries (e.g. install through npm install).

The most important file is your src/index.tsx file. This is the entry point of your React-app. The following code depicts the index.tsx we start with.

In this file, you export (as default) a <ServiceOrientedApp/>-component. The <ServiceOrientedApp/>-component takes only a few parameters.

  • The stackName is the (arbitrary) name of our app. If you plan to deploy it, the name must be unique across your AWS account. It should be lowercase and hyphens, only.

  • The buildPath is the relative path to the folder within your project where the build-resources are put, e.g. build. You may want to add this name to your .gitignore file to keep your repository free from compiled files.

  • The region is the AWS-region you want your infrastructure to reside after deployment, e.g. us-east-1.

The <ServiceOrientedApp/>-component takes two types of children:

  • <Environment/>-components specify runtime environments of your app. The name serves as the literal in scripts we work with. Thus, use short names, with no special characters other than hyphens.

  • <Route/>-components are the pages of your app. They work like the <Route/>s in react-router. They contain the user interface of your app. Everything you see. Everything you can interact with.

Once you have your project files in place, you can build it initially (npm run build). The definition of the build-script in your package.json is:

The build-step adds commands to your package.json to start and deploy your service-oriented app.

Let’s check whether the app works so far. We start it locally with npm run newline-file-management (replace newline-file-management by the name of your <ServiceOrientedApp/>-component). When you open localhost:3000 in your browser, you should see the text “Hello Infrastructure-Components!” So far so good. You can stop the app with ctrl+c.

npm run newline-file-management runs your front-end in hot-dev-mode. Any change you do in your code applies instantly. Just refresh your browser page. But this command does not start your back-end services and file-storage that we will develop in this post.

The command npm run start-dev starts the full software stack locally. But any change you do that affects a back-end resource requires you to stop (ctrl+c) and rerun the command.

Upload-Form#

We start with an upload form the user can use to add files from her computer. A simple <input type="file"/> would suffice. It displays a button and the filename of the selected file.

But I don't like its appearance. Why don't we apply our own style?

There are different ways of how we can style our components.

  1. We could add the components' styles by pointing a className-property to a CSS-class we specify in separate CSS-files. But global CSS is hard to manage.

  2. We could add inline styles through a component's style-property. But working with inline styles makes reuse cumbersome.

  3. We can use styled-components that combine the advantages of a global CSS and inline styles. Styled-components let you style your React components through local CSS literals. And it supports global themes. With styled-components, you can define the appearance of your components at the level of abstraction that best suits your problem at hand.

Let's create our <UploadForm/>-component in a new file: upload-form.tsx.

In this module, besides React, we import the styled-module. This is a low-level factory that provides helper methods in the form of styled.tagname. The tagname is any valid HTML-tag. For instance, styled.a creates a link-component (<a/>). The styled/styled.tagname-functions take the CSS definitions in a template string (enclosed by backticks `...`). A template string is a multi-line string that allows embedded expressions. You embed an expression through a dollar character and a pair of curly brackets, like this: ${...}.

At line 8, we define LabelFrame. This is a styled.label. We let it take the whole width, except for a small margin (lines 10-11). We let it center its children (lines 14-18). And we specify its general appearances, like colors, border, and the size of the font (lines 21-28).

At line 31, we define UploadInput. It is a styled.input that we always hide by setting display:none; But what sense does an element make when it is always hidden?

The answer is: we need the function of this HTML-element but not its visual representation. Once clicked, we want the browser to open the file-selection dialog. But we don't want to display the unsightly button. That's we styled our LabelFrame for.

The trick is in the UploadForm-function (lines 42-45). We render the LabelFrame and set its htmlFor-property to the id of the UploadInput. This connects both elements and the HTML-label (LabelFrame) acts on behalf of the HTML-input (UploadInput).

In our index.tsx, we need to render our newly created component:

Let's have a look. Start your app locally by npm run newline-file-management

We have a nice user interface, now. When you click on it, the browser's file-select dialog opens. And when you select a file, you can see the output in the console. That is what the onFileSelected-function does that we call in the onChange-property of the UploadInput. It takes and logs the first element of the event.target.files-array.

Upload the file to the File-Storage#

Uploading the file involves a series of technical steps. We need to load the binary data of the file and send it to a back-end service. This service must accept the file and write it to the S3 storage.

Technically, it would be possible to upload the file directly to S3 from the browser. But since the browser is the user's machine, we would need to grant public write access to our S3. With a web-service in between, we run code. Our code! We can control (if we like) who may or may not upload files.

But don't worry. Infrastructure-components provide the <Storage/>-component. This does all the heavy loading for us and lets us concentrate on the functional aspects.

We create a new module: file-storage.tsx

We define and export a configured <Storage/> (lines 29-34). It takes an id and a path as properties.

  • The id is the (arbitrary) name of your storage. It serves as identifier within your app and as the folder name at S3.

  • The path is the relative URL of the endpoint at which the service is reachable.

We define the upload-function that uploads a file (line 10). The upload-function takes the file and the onUpload-callback function. It calls the uploadFile-function (line 10) we import from infrastructure-components (line 5).

While the upload-function passes through the file and the onUpload-arguments, it connects the function to our <FileStorage/> by specifying its id (line 11). In the lines 16-20, we specify an onProgress-callback. The <Storage/> splits large files into chunks and uploads them one by one. After each chunk, you can decide on whether to proceed (return true) or cancel (return false). In this example, we print the current progress to the console and continue.

Further, we provide default values to the prefix, data, and onError arguments that we do not use.

We can easily integrate the storage now. In our index.tsx, we add the <FileStorage/>-component as a direct child of the <ServiceOrientedApp/>. I omitted the unchanged parts in this snippet.

Integrating the upload-function is as easy. Let's look at the upload-form.tsx.

We import the upload-function (line 2) and call it within our onFileSelected-function. We provide the file-object and a callback-function that prints "file uploaded" and the URI to the console once the upload is complete (lines 8-9).

Let's have a try. This time, we need to start the whole stack of our software (npm run start-dev)

You can see one "Uploading File" message (if the file you selected exceeds 100kb) and our "file uploaded" message. Let's have a look into the console of our IDE (or wherever you started the run-script from)

This contains all the console-output of our back-end. As you can see, it seems to have received multiple parts of the file and displays a message like this:

Stored object "FILESTORAGE/upload-button.png" in bucket "newline-file-management-dev" successfully

We can even check for the file on our local S3-storage. Because it runs at localhost:3002. Thus, make sure the port 3002 is available. Try to open the link you got back in your console after the upload. It triggers the download of the file you just uploaded.

List the existing files#

Of course, we want to display the files we uploaded in a list. We put all the code related to this into a new file: file-list.tsx.

Don't get overwhelmed by this snippet. To summarize, our list of files is a classic HTML-<ul/> (unordered list). For each file, it contains a link (<a/>) within a list-item (<li/>). Most of the code is styling. And a little bit loading the data.

Let's start with the styling.

  • We assign the same width and margin to our StyledList (the <ul/>-component) that we assigned to our UploadForm (lines 12-16).

  • We remove the enumeration symbol (the dot) of our Items and let them have a thin top-line (lines 18-21)

  • We let the FileLink take the whole width (display: block, line 24) and change the hovering-effect by removing the underline (text-decoration: none, line 25) and adding a background-color (lines 28-31).

Let's talk about how we load the data. Infrastructure-components provide the <FilesList/>-component. We import it in line 5. It wraps an internal API-call that requests the list of files from AWS S3. Again, we do not have to cope with heavy loading.

The <FilesList/> requires the following parameters:

  • the storageId is the id you specified the <Storage/>

  • the prefix is the relative path of the folder whose content to show. We use the same empty string we used when uploading the files.

  • the mode is one of LISTFILES_MODE.ALL, LISTFILES_MODE.FILES, or LISTFILES_MODE.FOLDERS. We only want to get the files. Thus, we specify LISTFILES_MODE.FILES.

<FilesList/> is a wrapper function. It does not render anything. But it provides data. We access this data by providing a function as the child:

<FileList>{ ( args ) => { function body } }</FileList>

The argument we get is an object. It has three keys: loading, files, and error.

  • loading is a Boolean value indicating whether the (internal) API-call request is currently running (true) or completed (false)

  • files is undefined until the API-call resolves successfully. Then it contains a list of objects. Each object contains:

    • file (the filename)

    • url (the link to the file on S3)

    • lastModified (the datetime of the file's last modification)

    • itemKey (the S3-internal path to the file)

  • error is undefined until the API-call gets rejected. Then error contains the error message.

Note: either files or error get a value. But not both.

We access these three keys directly by destructuring the object (using curly brackets {...}) (line 42). We return (line 44) a Boolean statement. This is a short form of an if-then-else construct.

If loading is a truthy value, we display the string "Loading". If not, but error contains a truthy value (like a valid error message string), we show the error message. If not, too, we render our StyledList (the <ul/>) (line 46) and "transform" each file-object into a rendered <Item/><FileLink/></Item>-construct (this is <li><a/></li> with styling) (lines 46-53).

An array's map-function lets you take an existing array and create a new array from it with transformed values — without changing the original array. In our case, we take the file-object and transform it into a React-component.

Let's have a look.

Update list automatically#

Does not look too bad, does it? But there's a small thing left to do. When you upload a file, the list does not update automatically. You have to refresh the browser. The reason is that the <FilesList/> fetches the data from S3 only once.

But it provides a callback-property that allows us to set a refetch-trigger. This is the property in line 40 we did not explain yet.

The optional property onSetRefetch takes a function. This function is called when <FilesList/> mounts (is added to the visual component-hierarchy). It passes the refetch-function as an argument. This refetch-function invalidates the results thus far and triggers the <FilesList/> to fetch the files again.

We take the onSetRefetch-function from the properties: onSetRefetch={props.setRefetch}. So, we need to provide it, right?

We use our <FileList/> in the index.tsx. Here's what we need to change. As usual, I skipped the parts we did not change.

The first thing we changed is that we do not render the <FileList/> and the <UploadForm/> inline of the <Route/>-component anymore. But we provide an explicit FileManagement-component. The reason is: we need to use a React-hook: useState. And React-hooks work only in functional React components (like FileManagement).

Hooks offer a way to “attach” reusable behavior to a component. useState is a function. It attaches a "state" to a functional component. We import it from react (line 1). It takes the initial value as a single argument. In our case: this initial value is undefined.

useState returns an array with two elements. The first element is the current value. Since we did not change the value yet, it is our initial value: undefined. The second element is a function that sets the value. We provide this function as the setRefetch-property of our <FileList/>. Thus, once the <FileList/> mounts, it calls the setRefetch-function. This puts the respective value (the "trigger the refetch"-function) into the first element — into refetch.

React does not change the value. But it triggers the re-rendering of the component-hierarchy with the new value. We provide the refetch-value (that is a function now) to the <UploadForm/>. Let's use it!

In our UploadForm, we call the upload-function in the onFileSelected-function. The upload-function takes the onUpload-callback. That is a function called when the upload is complete. This is a good place to trigger the refetch-function from the props (line 5).

You can now upload a file and see it is added directly after the upload completes.

Deploy your app#

Let’s deploy our app. Deploying your app requires some one-time setup:

1. An AWS account that you can create at https://aws.amazon.com

2. A technical user (with programmatic access / API-key)

In your AWS-console, open the IAM menu and create a new user with the following policy:

You’ll get an AWS Key Id and an AWS Secret Key.

3. Put these into the .env-file in your project root.

With this setup, you can deploy your app with a single command: npm run deploy-dev

The name of the <Environment/>-component appears in the scripts we can use to deploy your app: npm run deploy-dev(replace dev by the name of the <Environment/>).

Once the deployment is complete, you get back the URL of your app, that looks like https://{some-string}.execute-api.us-east-1.amazonaws.com/dev

Let's have a look at our app in action.


Clap
0|0