How to create and deploy a Full-Stack React application

Hello, Habr! I present to you the translation of the article “How to Build and Deploy a Full-Stack React-App” by Frank Zickert.



Infrastructure components make it easy to create, run, and deploy a full-fledged React application. With these React components, you can focus on writing the business logic of your application. You do not need to worry about its configuration.



Want to become a full-stack developer? The full-stack application complements the interactive React web interface with a server and database. But such an application requires much more settings than a simple one-page application.



We use infrastructure components . These React Components allow us to define our infrastructure architecture as part of our React application. We no longer need any other settings, such as Webpack, Babel or Serverless.



Start



You can set up your project in three ways:





Once you have installed the dependencies (run npm install



), you can build the project with one command: npm run build



.



The build script adds three more scripts to package.json:





Note. To deploy your application to AWS, you need an IAM technical user with these rights. Put the user credentials in your .env file as follows:



 AWS_ACCESS_KEY_ID = *** AWS_SECRET_ACCESS_KEY = ***
      
      





Define your application architecture



Projects based on infrastructure components have a clear structure. You have one top-level component. This defines the overall architecture of your application.



Subcomponents (child components) refine (extend) the behavior of the application and add functions.



In the following example, the <ServiceOrientedApp />



component is our top-level component. We export it as the default file to our entry point file ( src / index.tsx)



.



 export default ( <ServiceOrientedApp stackName = "soa-dl" buildPath = 'build' region='eu-west-1'> <Environment name="dev" /> <Route path='/' name='My Service-Oriented React App' render={()=><DataForm />} /> <DataLayer id="datalayer"> <UserEntry /> <GetUserService /> <AddUserService /> </DataLayer> </ServiceOrientedApp> );
      
      





<ServiceOrientedApp />



is an interactive web application. You can clarify (expand) the functionality of this application using the child components you provided. It supports the <Environment />



, <Route />



, <Service />



and <DataLayer />



.



<Envrionment />



determines the runtime of your application. For example, you can have dev and prod version. You can run and deploy each separately.



<Route />



is the page of your application. it works like <Route />



in react-router. Here is a tutorial on how to work with routes .



<Service />



defines a function that runs on the server side. It can have one or several <Middleware />



- components as children.



<Middleware />



works like Express.js-middleware.

<DataLayer />



adds a NoSQL database to your application. Accepts <Entry /> - components as children. <Entry /> describes the type of items in your database.



These components are all we need to create our full-stack application. As you can see, our application has: one runtime, one page, two services and a database with one record.



The component structure provides a clear view of your application. The larger your application becomes, the more important it is.



You may have noticed that <Service />



are children of <DataLayer />



. This has a simple explanation. We want our services to have access to the database. It's really that simple!



Database Design



<DataLayer />



creates Amazon DynamoDB. This is a key-value database (NoSQL). It provides high performance on any scale. But unlike relational databases, it does not support complex queries.



The database schema has three fields: primaryKey



, rangeKey



and data



. This is important because you need to know that you can only find entries by its keys. Either by primaryKey



, or by rangeKey



, or both.



With this knowledge, let's take a look at our <Entry />



:



 export const USER_ENTRY_ID = "user_entry"; export default function UserEntry (props) { return <Entry id={ USER_ENTRY_ID } primaryKey="username" rangeKey="userid" data={{ age: GraphQLString, address: GraphQLString }} /> };
      
      





<Entry />



describes the structure of our data. We define names for our primaryKey and rangeKey. You can use any name other than some DynamoDB keywords that you can find here. But the names we use have functional implications:





In our example, this means that:





Add items to the database



We defined two <Service />



components in our <ServiceOrientedApp />



. POST



service that adds the user to the database and GET



service that retrieves the user from it.



Let's start with <AddUserService />



. Here is the code for this service:



 import * as React from 'react'; import { callService, Middleware, mutate, Service, serviceWithDataLayer } from "infrastructure-components"; import { USER_ENTRY_ID, IUserEntry } from './user-entry'; const ADDUSER_SERVICE_ID = "adduser"; export default function AddUserService () { return <Service id={ ADDUSER_SERVICE_ID } path="/adduser" method="POST"> <Middleware callback={serviceWithDataLayer(async function (dataLayer, req, res, next) { const parsedBody: IUserEntry = JSON.parse(req.body); await mutate( dataLayer.client, dataLayer.setEntryMutation(USER_ENTRY_ID, parsedBody) ); res.status(200).set({ "Access-Control-Allow-Origin" : "*", // Required for CORS support to work }).send("ok"); })}/> </Service> };
      
      





The <Service />



component - accepts three parameters:





We add <Middleware />



as a child. This <Middleware />



takes a callback function as a parameter. We could directly provide Express.js middleware. Since we want to access the database, we serviceWithDataLayer



function in serviceWithDataLayer



. This adds dataLayer



as the first parameter to our callback.



DataLayer



provides access to the database. Let's see how!



The mutate



asynchronous function applies the changes to the data in our database. This requires a client and a mutation command as parameters.



Element data is a Javascript object that has all the necessary key-value pairs. In our service, we get this object from the request body. For User



object has the following structure:



 export interface IUserEntry { username: string, userid: string, age: string, address: string }
      
      





This object accepts the names primaryKey



and rangeKey



and all the data keys that we defined in <Entry />



.



Note: for now, the only supported type is a string that matches the GraphQLString in the definition of <Entry />.

We mentioned above that we take IUserEntry



data from the body. How does this happen?



Infrastructure components provide the asynchronous function callService (serviceId, dataObject)



. This function accepts a service identifier, a Javascript object (for sending as a request body when using POST



), a success



function, and an error callback function.



The following snippet shows how we use this function to call our <AddUserService />



. We specify serviceId



. And we pass userData



, which we take as a parameter to our function.



 export async function callAddUserService (userData: IUserEntry) { await callService( ADDUSER_SERVICE_ID, userData, (data: any) => { console.log("received data: ", data); }, (error) => { console.log("error: " , error) } ); };
      
      





Now the callAddUserService



function is all we need when we want to add a new user. For example, call it when the user clicks a button:



 <button onClick={() => callAddUserService({ username: username, userid: userid, age: age, address: address })}>Save</button>
      
      





We simply call it using the IUserEntry



object. It calls the correct service (as indicated by its identifier ( id



)). It puts userData



in the request body. <AddUserService />



takes data from the body and puts it in the database.



Get items from the database



Retrieving items from a database is as easy as adding them.



 export default function GetUserService () { return <Service id={ GETUSER_SERVICE_ID } path="/getuser" method="GET"> <Middleware callback={serviceWithDataLayer(async function (dataLayer, req, res, next) { const data = await select( dataLayer.client, dataLayer.getEntryQuery(USER_ENTRY_ID, { username: req.query.username, userid: req.query.userid }) ); res.status(200).set({ "Access-Control-Allow-Origin" : "*", // Required for CORS support to work }).send(JSON.stringify(data)); })}/> </Service> }
      
      





Again, we use <Service />, <Middleware /> and a callback function with access to the database.



Instead of the mutate



function, which adds an item to the database, we use the select



function. This function asks for the client we are taking from dataLayer



. The second parameter is the select



command. Like the mutation



command, we can create a select



command using dataLayer



.



This time we use the getEntryQuery



function. We provide the identifier ( id



) <Entry />



whose element we want to receive. And we provide the keys ( primaryKey



and rangeKey



) of a particular element in a Javascript object. Since we provide both keys, we get one element back. If it exists.



As you can see, we take the key values ​​from the request. But this time we take them from request.query



, and not from request.body



. The reason is that this service uses the GET



method. This method does not support the body in the request. But it provides all the data as query parameters.



The callService



function handles this for us. As in the callAddUserService-function



, we provide the identifier ( id



) <Service />



that we want to call. We provide the necessary data. Here it is only the keys. And we provide callback functions.



A successful callback provides a response. The response body in json format contains our element found. We can access this element through the get_user_entry



key. " Get_



" defines the query that we placed in our select function. " User_entry



" is the key of our <Entry />



.



 export async function callGetUserService (username: string, userid: string, onData: (userData: IUserEntry) => void) { await callService( GETUSER_SERVICE_ID, { username: username, userid: userid }, async function (response: any) { await response.json().then(function(data) { console.log(data[`get_${USER_ENTRY_ID}`]); onData(data[`get_${USER_ENTRY_ID}`]); }); }, (error) => { console.log("error: " , error) } ); }
      
      





Take a look at your Full-Stack app in action.



If you haven't started your application yet, now is the time to do it: npm run start-{your-env-name}



.



You can even deploy your application to AWS with a single command: npm run deploy-{your-env-name}



. (Remember to put the AWS credentials in the .env file).



This post does not describe how you enter the data that you put into the database and how you display the results. callAddUserService



and callGetUserService



encapsulate everything that is specific to services and the database. You just put the Javascript object there and get it back.



You will find the source code for this example in this GitHub repository . It includes a very simple user interface.



All Articles