You can set up your project in three ways:
Once you installed the dependencies (run npm install
if you cloned or downloaded the files), you can build the project with a single command: npm run build
.The build script adds three more scripts to the package.json
. They are ready to use!
Infrastructure-Components-based projects have a clear structure. You have a single top-level component. This defines the overall architecture of your app. Sub-components (children) refine the app’s behavior and add functions.
In the following example, the <ServiceOrientedApp/>
-component is our top-level component. We export it as default in 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>
);
These components are all we need to build our full-stack app. As you can see, our exemplary app has
The structure of components provides a clear overview of your app. The bigger your app gets, the more important this is.
You might have noticed that the <Service/>
s are children of the <DataLayer/>
. This has a simple reason. We want our services to have access to the database. It really is that easy!
The <DataLayer/>
creates an Amazon DynamoDB. This is a key-value database (NoSQL). It delivers high performance at any scale. But unlike relational databases, it does not support complex queries.
The schema of the database has three fields: primaryKey
, rangeKey
and data
. This is important because you need to know that you can only find entries through its keys. Either the primaryKey
, the rangeKey
, or both.
With this knowledge, let’s have 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
}}
/>
};
The <Entry/>
describes the structure of our data. We define names for our primaryKey
and rangeKey
. You can use any name except for some DynamoDB keywords that you can find here. But the names we use have functional implications:
In our example, this means that:
We defined two <Service/>
-components in our <ServiceOrientedApp/>
. A POST
-service that adds a User
to the database and a GET
-service that retrieves a User
from it.
Let’s start with the <AddUserService/>
. Here’s the code of this service. We’ll go through it right away.
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 takes three parameters.
We add a <Middleware/>
as a child. This <Middleware/>
takes a callback function as a parameter. We could directly provide an Express.js-middleware here. For we want to access the database, we wrap the function into serviceWithDataLayer
. This adds dataLayer
as the first parameter to our callback.
The dataLayer
provides access to the database. Let’s see how!
The asynchronous function mutate
applies changes to the data in our database. It requires a client
and a mutation
-command as parameters.
The data
of the item is a Javascript object that has all the required key-value pairs. In our service, we get this object passed from the request body (we cover this in a minute). For the User
, the object has the following structure:
export interface IUserEntry {
username: string,
userid: string,
age: string,
address: string
}
This object takes the names of the primaryKey
and the rangeKey
and all the keys of the data
that we defined in the <Entry/>
.
Note: as of now, the only supported type is *string*
that corresponds to*GraphQLString*
* in the *<Entry/>*
‘s definition.*
We mentioned above that we take the IUserEntry
-data from the body. How does it get there?
Infrastructure-Components provide the async function callService(serviceId, dataObject)
. This function takes the service-id
, a Javascript-object (to be sent as the request body when using POST
), a success-, and an error callback function.
The following snippet shows how we use this function to call our <AddUserService/>
. We specify the service-id
. And we pass through the userData
that we take as a parameter for 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 instance, call it when the user clicks a button:
<button onClick={() => callAddUserService({
username: username,
userid: userid,
age: age,
address: address
})}>Save</button>
We just call it with an IUserEntry
-object. It calls the right service (as specified by its id
). It puts the userData
into the body of the request. The <AddUserService/>
takes the data from the body and puts it into the database.
Retrieving items from the 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 a <Service/>
, a <Middleware/>
and a callback-function with database access.
Instead of the mutate
-function that adds an item to the database, we use the select
-function. This function requires the client
that we take from the dataLayer
. The second parameter is the select
-command. Like a mutation
-command, we can create a select
-command with the help of the dataLayer
.
This time, we use the getEntryQuery
-function. We provide the id
of the <Entry/>
whose item we want to get. And we provide the keys (primaryKey
and rangeKey
) of the specific item in a Javascript object. For we provide both keys, we get a single item back. If it exists.
As you might have seen, we take the key-values from the request. But this time, we take them from the request.query
rather than from the request.body
. The reason is that this service uses the GET
-method. This method does not support a body in the request. But it provides all the data as query parameters.
The callService
function handles that for us. Like in the callAddUserService
-function, we provide the id
of the <Service/>
we want to call. We provide the required data. Here it is only the keys. And we provide callback functions.
The success-callback provides the response
. The json-formatted body of the response
contains our retrieved item. We can access this item through the key get*user*entry
. “get_” specifies the query we have put into 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)
}
);
}
See Your Full-Stack App in Action
If you have not started your app yet, it’s a good time to do it now: npm run start-{your-env-name}
.
You can even deploy your app to AWS with a single command: npm run deploy-{your-env-name}
. (Don’t forget to put the AWS credentials into the .env
-file).
This post does not cover how you enter the data you put into the database and how you display the results. callAddUserService
and callGetUserService
encapsulate everything that is specific to the services and the database. You just put a Javascript object in there and get it back.
You’ll find the source code of this example in this GitHub-repository. It includes a very basic user interface.
☞ The Image Processing Tutorial from Zero to One
☞ React Native Tutorial for Beginners - Crash Course 2019
☞ Vuejs is Good ! But Is It Better Than Angular or React?
☞ An Intro to Redux that you can understand
☞ Setting React - Laravel Without using Laravel mix
☞ How to set up a TypeScript + Gatsby app
☞ Getting started with react-select
☞ Understanding State and Props in ReactJS
#reactjs #javascript