A Deep Dive Into Serverless UI With TypeScript

Quick summary ↬ Serverless UI is simply a free, open-source command-line utility for quickly building and deploying serverless applications on the AWS platform. In this article, we will learn and cover everything needed on using Serverless UI to deploy our projects or serverless applications to cloud services providers.

If you’ve been looking for a clear explanation of how applications can be developed and deployed to AWS with less configuration as possible, then I’ve prepared just the article for you. We’ll be breaking it all down into two parts: deploying a static web application (in this case a Notes application), and then a serverless web application to CloudFront using the Serverless UI library.

Note: To follow along, you’ll need a basic understanding of AWS and web development in order to understand how the TypeScript project is built and used to deploy to AWS.

Requirements

Before starting to build our project, the following requirements need to be met:

  • Basic knowledge of React, React Hooks, and Material UI;
  • Good knowledge of TypeScript;
  • Node.js version >= 12.x.x installed on your local machine;
  • Have an AWS verified account;
  • Configured your AWS CLI with local credentials;
  • Ensure that npm or yarn is also installed as the package manager.

Introduction

We’ll start with a few introductions on Serverless UI, but at the end of this tutorial, you should be able to comfortably use Serverless UI in your applications — from installing to understanding the concepts and implementing it in your very own projects. According to the docs on GitHub:

“Serverless UI is simply a free, open-source command-line utility for quickly building and deploying serverless applications on the AWS platform.”

As stated, it’s a lightweight library that’s quickly installed over the terminal, and can be used to set up configure-domain, deploy static or serverless websites — all done on the terminal. This permits you to easily couple any choice of front-end framework with Serverless UI to deploy existing and new applications to AWS stress-free.

Serverless UI also works great with any static website, and websites that use serverless functions to handle requests to some sort of API. This makes it great for building serverless back-end applications. The deploy process through Serverless UI gives you the control to automatically deploy each part or in better words, iteration of your application with a different and separate URL. Though, this means you get to monitor the continuous integration and testing of your application with confidence in real-time.

Using Serverless UI in production, you can choose to have your project or serverless functions written in native JavaScript or TypeScript. Either way, they’ll be bundled down extremely quickly and your functions deployed as Node.js 14 Lambda functions. Your functions within the ./functions folder are deployed automatically as serverless functions on AWS. This approach means that we’ll be writing our code in the form of functions that will handle different tasks or requests within the application. So when we deploy our functions, we’ll invoke them in the format of an event.

Then the need for a fast and very small application file size makes the Serverless UI be of good essence within our application. Being a command-line tool, it doesn’t need to be bundled inside the application — it can be installed globally, npm install -g @serverlessui/cli or as a devDependency within our application. This means no file size was added to our application, giving us the benefit of having only the code needed for our application to function. No extra added bundle size to our application. As with any migration, we developers know that migrating existing applications can be tough and troubling without downtime for our users, but it is doable depending on the use case.

More after jump! Continue reading below ↓

Pros And Cons Of Using Serverless UI

Using Serverless UI within our projects, whether existing or new project has some benefits that it gives us:

  • There are no middleman services unlike others; Serverless UI gives you out-of-the-box benefits of a pre-configured infrastructure without having to go through a middleman.
  • It supports and works in almost any CI (Continuous Integration) environment owing that it’s a command-line tool readily available via npm. This is a plus for the backend and infrastructure setup.
  • For already existing serverless applications or those that may have additional CloudFormation and/or CDK infrastructure, there is a full provision of CDK constructs for each of the CLI actions.
  • Serverless UI provides almost any option during deploying your application — deploy your static website, Lambda functions or production code.
  • Almost all configurations (such as configure-domain and deploying applications) are all done on the command line.
  • Front-end frameworks like React, Svelte, Vue, or JQuery are all supported, as long as it compiles down to static code.
  • Gives serverless applications the ability to scale dynamically per request, and won’t require any capacity planning or provisioning for the application.

These are some downsides of Serverless UI that we should consider before deciding to use it within our projects:

  • There is only support for projects built using TypeScript or JavaScript within the project.
  • Within recent time, the library core infrastructure is written with aws-cdk, which means the only platform our applications could be deployed to is AWS.

Recommended Reading: Local Testing A Serverless API (API Gateway And Lambda)

Setting Up The Notes Application

Nowadays, several tools are available for developers to efficiently manage infrastructures, for example, the Serverless UI, the console, or one of the frameworks available online. As explained above, our goal is to set up a simple demo of a Notes application in TypeScript, which will quickly help us to demonstrate how Serverless UI could be used in hosting it, so you can quickly grasp and implement it within your own projects.

For this tutorial, we’ll quickly explore and explain the different parts of a Notes application, then install Serverless UI library to host the application on AWS.

We proceed to clone the remote repository on our local machine and run the command that will install all the dependencies.

git clone https://github.com/smashingmagazine/serverless-UI-typescript.git yarn install

The above command clones a Note application that has the functional components built already, and then goes ahead to install the dependencies that are needed for the components to function. Here’s the list of the dependencies that are required for this Notes application to function:

{ ... "dependencies": { "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.15", "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-scripts": "4.0.3", "typescript": "^4.1.2", "web-vitals": "^1.0.1" }, ...
}

The above list contains dependencies and their type definitions to work optimally with TypeScript. We proceed to explain the working parts of the application. But let’s first define interfaces for the Note data and the Props argument that will be passed down into our functions. Create a /src/interfaces.ts file and include the following:

export interface INote { note: string;
}
export interface Props { content: INote; delContent(noteToDelete: string): void;
}

Here we’re defining the type structure that acts as a syntax contract between our components and the props passed within them. Also defines the unit data of our application state, INote.

For this application, we’ll focus mainly on the /src/components folder and the /src/App.tsx file. We’ll start from the components folder then gradually explain the rest of the application.

Note: The styles defined and used throughout this Notes application can be found in the /src/App.css file.

The components folder contains one file, the Note.tsx file; which will define the UI structure of each Note data we create.

import { INote } from "../Interfaces"; interface Props { content: INote; delContent(noteToDelete: number): void;
} const Note = ({ content, delContent }: Props) => { return ( <div className="note"> <div className="content"> <span>{content.note}</span> </div> <button onClick={() => { delContent(content.id); }} > X </button> </div> );
};
export default Note;

Within the Note function, we’re destructuring a props parameter that has the data type definition of Props, and contains the content and delContent fields. The content field further contains the note field whose value will be the input value of our users. While the delContent field is a function to delete content from the application.

We’ll proceed to build the general UI of the application, defining its two sections; one for creating the notes and the other to contain the list of notes already created:

const App: FC = () => { return ( <div className="App"> <div className="header"> </div> <div className="noteList"> </div> </div> );
};
export default App;

The div tag with the header class contains the input and the button elements for creating and adding notes to the application:

const App: FC = () => { return ( <div className="App"> <div className="header"> <div className="inputContainer"> <input type="text" placeholder="Add Note..." name="note" value={noteContent} onChange={handleChange} /> </div> <button onClick={addNote}>Add Note</button> </div> ... </div> );
};
export default App;

In the above code we recorded a new state, noteContent, for the input element’s value. Also an onChange event to update the input value. The button element has onClick event that will handle generating new content from the input’s value and adding it to the application. The above UI markup coupled with the already defined styles will look like:

The header component
Header component. (Large preview)

Now let’s define the new states, noteContent and noteList, then the two events, handleChange and addNote functions to update our application functionalities:

import { FC, ChangeEvent, useState } from "react";
import "./App.css";
import { INote } from "./Interfaces"; const App: FC = () => { const [noteContent, setNoteContent] = useState<string>(""); const [noteList, setNoteList] = useState<INote[]>([]); const handleChange = (event: ChangeEvent<HTMLInputElement>) => { setNoteContent(event.target.value.trim()); }; const addNote = (): void => { const newContent = { Date.now(), note: noteContent }; setNoteList([...noteList, newContent]); setNoteContent(""); }; return ( <div className="App"> <div className="header"> <div className="inputContainer"> <input type="text" placeholder="Add Note..." name="note" value={noteContent} onChange={handleChange} /> </div> <button onClick={addNote}>Add Note</button> </div> ... </div> );
};
export default App;

The noteList state contains all the notes created within the application. We add and remove from it to update the UI with more notes created. Within the handleChange function, we’re regularly updating noteContent with the changes made to the input field using the setNoteContent function. The addNote function creates a newContent object with a note field whose value is gotten from noteContent. It then calls the setNoteList functions and creates a new noteList array from its previous state and newContent.

Next is to update the second section of the App function with the JSX code to contain the list of notes created:

... import Note from "./Components/Note"; const App: FC = () => { ... return ( <div className="App"> <div className="header"> ... </div> <div className="noteList"> {noteList.map((content: INote) => { return <Note key={content.id} content={content} delContent={delContent} />; })} </div> </div> );
}; export default App;

We’re looping through the noteList using the Array.prototype.map method to create the dump of notes within our application. Then we imported the Note component which defines the UI of our note, passing the key, content and delContent props into it. The delContent function as discussed earlier deletes content from the application:

...
import Note from "./Components/Note"; const App: FC = () => { ... const [noteList, setNoteList] = useState<INote[]>([]); ... const delContent = (noteID: number) => { setNoteList( noteList.filter((content) => { return content.id !== noteID; }) ); }; return ( <div className="App"> <div className="header"> ... </div> <div className="noteList"> {noteList.map((content: INote) => { return <Note key={content.id} content={content} delContent={delContent} />; })} </div> </div> );
};
export default App;

The delContent function filters out of noteList the contents that are not in any way equivalent to the noteToDelete argument. The noteToDelete is equivalent to content.note but gets passed down to delContent whenever a note is created by calling the Note component.

Coupling the two sections of the App component together, your code should look like the below:

import { FC, ChangeEvent, useState } from "react";
import "./App.css";
import Note from "./Components/Note";
import { INote } from "./Interfaces"; const App: FC = () => { const [noteContent, setNoteContent] = useState<string>(""); const [noteList, setNoteList] = useState<INote[]>([]); const handleChange = (event: ChangeEvent<HTMLInputElement>) => { setNoteContent(event.target.value.trim()); }; const addNote = (): void => { const newContent = { id: Date.now(), note: noteContent }; setNoteList([...noteList, newContent]); setNoteContent(""); }; const delContent = (noteID: number): void => { setNoteList( noteList.filter((content) => { return content.id !== noteID; }) ); }; return ( <div className="App"> <div className="header"> <div className="inputContainer"> <input type="text" placeholder="Add Note..." name="note" value={noteContent} onChange={handleChange} /> </div> <button onClick={addNote}>Add Note</button> </div> <div className="noteList"> {noteList.map((content: INote) => { return <Note key={content.id} content={content} delContent={delContent} />; })} </div> </div> );
};
export default App;

And if we go ahead and add a few notes to our application, then our final UI will look like this:

The Notes application
Notes application. (Large preview)

Now we have created a simple Notes application that we can add and delete Notes, let’s move on to using Serverless UI to deploy this application to AWS and as well deploy a serverless back-end application (serverless functions).

Deploying Notes Application With Serverless UI

Now we’re done explaining the components that make up our Notes application, it’s time to deploy our application using Serverless UI on the terminal. The first step in deploying our application to AWS is to configure the AWS CLI on our machine. Check here for comprehensive steps to take.

Next is to install the Serverless UI library globally on our local machine:

npm install -g @serverlessui/cli

This installs the package globally, meaning no extra file size was added within the build code.

Next is to make a build folder of the project, this is the folder we’ll reference within our terminal:

sui deploy --dir="build"
...
❯ Website Url: https://xxxxx.cloudfront.net

But for our project, we’ll run the yarn command that builds our application into a static website within the build folder, after which we run the Serverless UI command to deploy the application:

yarn build ...
Done in 80.63s. sui deploy --dir="build"
... ✅ ServerlessUIAppPreview1c9ec9f1 Outputs:
ServerlessUIAppPreview1c9ec9f1.ServerlessUIBaseUrlCA2DC891 = https://dal254gl37fow.cloudfront.net Stack ARN:
arn:aws:cloudformation:us-west-2:261955174750:stack/ServerlessUIAppPreview1c9ec9f1/e4dc82e0-fe44-11eb-b959-064619847e85

Our application was successfully deployed, and the total time it took to deploy was less than five minutes. The application was deployed to Cloudfront here.

Deploying Serverless Functions With Serverless UI

Here, we’ll focus on deploying Lambda functions written in our local environment, other than on the IDE provided on the AWS web platform. With Serverless UI, we’ll remove the hassle of doing a lot of configuration and set up before deploying it on AWS.

You’ll also want to ensure your local environment is as close to the production environment as possible. This includes the runtime, Node.js version. As a reminder, you need to install a version of Node.js supported by AWS Lambda.

The code or the /serverless folder used within this part of the article can be found here. This folder contains the source file, that makes a request to an API to get a random note; a joke.

const nodefetch = require("node-fetch"); exports.handler = async (event, context) => { const url = "https://icanhazdadjoke.com/"; try { const jokeStream = await nodefetch(url, { headers: { Accept: "application/json" } }); const jsonJoke = await jokeStream.json(); return { statusCode: 200, body: JSON.stringify(jsonJoke) }; } catch (err) { return { statusCode: 422, body: err.stack }; }
};

Before we deploy the serverless folder, we’ll need to install esbuild library. This will help make bundling of the application files more fast and accessible.

npm install esbuild --save-dev

The next step to deploy the serverless function on AWS is by specifying the folder location with the --functions flag as we previously did with the --dist flag when deploying our static website.

sui deploy --functions="serverless"

While the above command helps us build our application, the serverless function successfully deploys it:

... ✅ ServerlessUIAppPreview560dbd41 Outputs:
ServerlessUIAppPreview560dbd41.ServerlessUIFunctionPathjokesD9F032B9 = https://dwh6k64yrlqcn.cloudfront.net/api/jokes Stack ARN:
arn:aws:cloudformation:us-west-2:261955174750:stack/ServerlessUIAppPreview560dbd41/21de6780-fb93-11eb-a0fb-061a2a83f0b9
  • The serverless function is now deployed to Cloudfront here.

As a side note, we should be able to reference our API URL by relative path in our UI code like /api/jokes instead of the full URL if deployed at the same time with the /dist or /build folder. This should always work — even with CORS — since the UI and API are on the same domain.

But by default, Serverless Ui will create a new stack for every preview deployed, which means each URL will be different and unique. In order to deploy to the same URL multiple times, the --prod flag needs to be passed.

sui deploy --prod --dir="dist" --functions="serverless"

Let’s create a /src/components/Quote folder and inside it create an index.tsx file. This contains the JSX code to house the quotes.

import { useState } from "react"; const Quote = () => { const [joke, setJoke] = useState<string>(); return ( <div className="container"> <p className="fade-in">{joke}</p> </div> );
};
export default Quote;

Next, we will make a request to the deployed serverless functions to retrieve a joke from it within a set interval of time. This way the note, i.e the joke, within the <p className="fade-in">{joke}</p> JSX markup gets updated every 2000 milliseconds.

import { useEffect, useState } from "react"; const Quote = () => { const [joke, setJoke] = useState<string>(); useEffect(() => { const getRandomJokeEveryTwoSeconds = setInterval(async () => { const url = process.env.API_LINK || "https://dwh6k64yrlqcn.cloudfront.net/api/jokes"; const jokeStream = await fetch(url); const res = await jokeStream.json(); const joke = res.joke; setJoke(joke); }, 2000); return () => { clearInterval(getRandomJokeEveryTwoSeconds); }; }, []); return ( <div className="container"> <p className="fade-in">{joke}</p> </div> );
};
export default Quote;

The code snippet added to the above source code will use useEffect hook to make API calls to the serverless functions, updating the UI with the jokes returned from the request by using the setJoke function provided from the useState hook.

Let’s restart our local development server to see the new changes added to our UI:

Incorporated a serverless function that runs every two seconds in the Notes application
Notes application with a serverless function running on it. (Large preview)

Before deploying the updates to your existing application, you can set up a custom domain, and using Serverless UI deploy and push subsequent code updates to this custom domain.

Configure Domain With Serverless UI

We can deploy our serverless application to our custom domain rather than the default one provided by CloudFront. Configuring and deploying to our custom domain may take 20 – 48 hours to fully propagate but only needs to be completed once. Navigate into your project directory and run the command:

sui configure-domain --domain="<custom-domain.com>"

Replace the above value of the --domain flag with your own custom URL. Then you can continuously update the already deployed project by adding the --prod flag when using the sui deploy command again.

Recommended Reading: Building A Serverless Contact Form For Your Static Site

Conclusion

In this article, we introduced Serverless UI by discussing different merits that make it a good fit for deploying your application with it. Also, we created a demo of a simple Notes application and deployed it with the library. You can further build back-end serverless functions that are triggered by events happening with the application, and deploy them to your AWS lambda.

For the advanced use case of Serverless UI, we configured the default domain provided by CloudFront with our own custom domain name using Serverless UI. And for existing serverless projects or those that may have additional CloudFormation and/or CDK infrastructure, Serverless UI provides CDK constructs for each of the CLI actions. And with Serverless UI, we can easily configure a private S3 bucket — an extra desired feature for enhanced security on our serverless applications. Click here to read up more on it.

  • The code used within this article can be found on Github.

Resources

Smashing Editorial (ks, vf, yk, il)