Creating admin-like web applications with NestJS and React Admin. Part 2.

Introduction

In the first part, we showed how React Admin and NestJS can work together to quickly create a simple admin panel.

In this part, we will go a little bit beyond basic CRUD and cover the following topics:

  1. Handling one-to-many relationships
  2. Handling file uploads to Amazon S3
  3. Using the Google Maps input

Handling one-to-many relationships

To showcase this (and other points), we will change the domain model of our application. So instead of a guest list, we will manage companies and their addresses.

This will require the following entities and controllers to added to our API:

  • Company Entity with a declared relationship with Address Entity:
import {
Entity,
Column,
PrimaryGeneratedColumn,
BaseEntity,
OneToMany,
} from 'typeorm';
import { Type } from 'class-transformer';
import { AddressEntity } from '../address/address.entity';@Entity({ name: 'company' })
export class CompanyEntity extends BaseEntity {
@PrimaryGeneratedColumn()
id: number; @Column()
name: string; @Column()
description: string; @Column({
type: 'json',
comment: `[{ name: "string", url: "string"}]`,
nullable: true,
})
cover: Array<{ name: string, url: string }>; @OneToMany((type) => AddressEntity, (address) => address.company)
@Type((t) => AddressEntity)
addresses: AddressEntity[];

}
  • Address Entity:
import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
BaseEntity,
} from 'typeorm';
import { CompanyEntity } from '../company/company.entity';@Entity({ name: 'address' })
export class AddressEntity extends BaseEntity {
@PrimaryGeneratedColumn()
id: number; @Column()
name: string; @Column()
city: string; @Column()
street: string; @Column()
zip: string; @Column()
notes: string; @Column({ nullable: true })
companyId: number;
@ManyToOne(
(type) => CompanyEntity,
(company) => company.addresses
)
company: CompanyEntity;

}

The controllers for these entities are created using @nestjsx/crud. You can see how this can be done in the previous article.

From the admin UI perspective, we have to add new resources:

import React from 'react';
import { Admin, Resource } from 'react-admin';
import crudProvider from '@fusionworks/ra-data-nest-crud';
import companies from './Companies';
import addresses from './Addresses';
import { url } from './config/connection';const dataProvider = crudProvider(url);const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="company" {...companies} />
<Resource name="address" {...addresses} />
</Admin>
);export default App;

Add a file with views for address entity:

import React from 'react';
import {
Create,
SimpleForm,
TextInput,
Edit,
required,
TabbedShowLayout,
Tab,
TextField,
Show,
ReferenceField,
List,
Datagrid,
ReferenceInput,
SelectInput,
} from 'react-admin';const validateRequired = required();export const AddressCreate = props => {
const { location } = props;
const { companyId } = parse(location.search);
const redirect = companyId ? `/company/${companyId}/show/addresses`: false; return (
<Create {...props}>
<SimpleForm redirect={redirect}>
<TextInput source="name" validate={validateRequired} />
<TextInput source="city" validate={validateRequired} />
<TextInput source="street" validate={validateRequired} />
<TextInput source="zip" validate={validateRequired} />
<TextInput source="notes" validate={validateRequired} />
{companyId ? null : (
<ReferenceInput source="companyId" reference="company">
<SelectInput optionText="name" optionValue="id" />
</ReferenceInput>
)}
</SimpleForm>
</Create>
);
};const AddressTitle = ({ record }) => (<span>{`${record.name}`}</span>);export const AddressEdit = props => (
<Edit {...props} title={<AddressTitle />}>
<SimpleForm redirect={(basePath, id, data) =>
`/company/${data.companyId}/show/addresses`}>
<TextInput source="name" validate={validateRequired} />
<TextInput source="city" validate={validateRequired} />
<TextInput source="street" validate={validateRequired} />
<TextInput source="zip" validate={validateRequired} />
<TextInput source="notes" validate={validateRequired} />
</SimpleForm>
</Edit>
);export const AddressList = props => (
<List {...props}>
<Datagrid rowClick="show">
<TextField source="name" />
<TextField source="city" />
<TextField source="street" />
<TextField source="zip" />
<TextField source="notes" />
<ReferenceField
source="companyId"
reference="company"
linkType="show"
>
<TextField source="name" />
</ReferenceField>
</Datagrid>
</List>
);export const AddressShow = props => (
<Show {...props}>
<TabbedShowLayout>
<Tab label="resources.summary.name">
<TextField source="name" />
<TextField source="city" />
<TextField source="street" />
<TextField source="zip" />
<TextField source="notes" />
<ReferenceField
source="companyId"
reference="company"
linkType="show"
>
<TextField source="name" />
</ReferenceField>
</Tab>
</TabbedShowLayout>
</Show>
);export default {
list: AddressList,
create: AddressCreate,
edit: AddressEdit,
show: AddressShow,
};

Convert the company view form to a tabbed interface and add a control for displaying existing addresses to the second tab:

import React from 'react';
import randomstring from 'randomstring';
import { S3FileInput, S3FileField } from '@fusionworks/ra-s3-input';
import {
TabbedShowLayout,
Tab,
List,
Create,
SimpleForm,
Edit,
Show,
ReferenceManyField,
Datagrid,
EditButton,
TextField,
TextInput,
required,
} from 'react-admin';
import { url } from '../config/connection';
import AddAddressButton from './AddAddressButton';const validateRequired = required();const CompanyCreate = (props) => (
<Create {...props}>
<SimpleForm redirect="show">
<TextInput source="name" />
<TextInput source="description" />
</SimpleForm>
</Create>
);const CompanyTitle = ({ record }) => (<span>{record.name}</span>);const CompanyEdit = props => (
<Edit {...props} title={<CompanyTitle />}>
<SimpleForm redirect="list">
<TextInput source="name" />
<TextInput source="description" />
</SimpleForm>
</Edit>
);const CompanyShow = props => (
<Show {...props} title={<CompanyTitle />}>
<TabbedShowLayout>
<Tab label="summary">
<TextField source="name" />
<TextField source="description" />
</Tab>
<Tab label="addresses" path="addresses">
<ReferenceManyField reference="address" target="companyId" addLabel={false}>
<Datagrid>
<TextField source="name" />
<TextField source="city" />
<TextField source="street" />
<TextField source="notes" />
<EditButton />
</Datagrid>
</ReferenceManyField>
<AddAddressButton />
</Tab>
</TabbedShowLayout>
</Show>
);const CompanyDescription = ({ record }) => {
return <div>{record.description}</div>;
};
const CompanyList = props => (
<List {...props}>
<Datagrid rowClick="show" expand={<CompanyDescription />}>
<TextField source="name" />
</Datagrid>
</List>
);export default {
list: CompanyList,
create: CompanyCreate,
edit: CompanyEdit,
show: CompanyShow,
};

And finally, add an “Add address” button which will open the create address form:

import React from 'react';
import { Link } from 'react-router-dom';
import { withStyles } from '@material-ui/core/styles';
import { Button } from 'react-admin';const styles = {
button: {
marginTop: '1em',
},
};const AddAddressButton = ({ classes, record }) => (
<Button
className={classes.button}
variant="raised"
component={Link}
to={`/address/create?companyId=${record.id}`}
label="Add an address"
title="Add an address"
/>
);export default withStyles(styles)(AddAddressButton);

Handling file uploads to Amazon S3

Let’s say we would like to add some photos to our company record. We will store them on Amazon S3. To handle it from the UI perspective, we created a custom React Admin input — https://github.com/FusionWorks/react-admin-s3-file-upload/. Here is how it should be added to our application:

import randomstring from 'randomstring';
import { S3FileInput, S3FileField } from '@fusionworks/ra-s3-input';
import { url } from '../config/connection';
...
const CompanyCreate = (props) => (
<Create {...props}>
<SimpleForm redirect="show">
<TextInput source="name" />
<TextInput source="description" />
<S3FileInput
apiRoot={url}
source="cover"
validate={validateRequired}
uploadOptions={{
signingUrl: `${url}/s3/sign`,
s3path: `NestJsAdminBoilerplate/${randomstring.generate(10)}`,
multiple: true,
accept: 'image/*',
}}
multipleFiles
/>

</SimpleForm>
</Create>
);const CompanyTitle = ({ record }) => (<span>{record.name}</span>);const CompanyEdit = props => (
<Edit {...props} title={<CompanyTitle />}>
<SimpleForm redirect="list">
<TextInput source="name" />
<TextInput source="description" />
<S3FileInput
apiRoot={url}
source="cover"
validate={validateRequired}
uploadOptions={{
signingUrl: `${url}/s3/sign`,
s3path: `NestJsAdminBoilerplate/${randomstring.generate(10)}`,
multiple: true,
accept: 'image/*',
}}
multipleFiles
/>

</SimpleForm>
</Edit>
);const CompanyShow = props => (
<Show {...props} title={<CompanyTitle />}>
<TabbedShowLayout>
<Tab label="summary">
<TextField source="name" />
<TextField source="description" />
<S3FileField apiRoot={url} source="cover" />
</Tab>
<Tab label="addresses" path="addresses">
....

</Tab>
</TabbedShowLayout>
</Show>
);...

To make this component work, we will need some support from the API side for signing AWS API requests. Let’s add the required components to our NestJS-based backend.

Service:

import { Injectable } from '@nestjs/common';
import { InjectConfig, ConfigService } from 'nestjs-config';
import { S3 } from 'aws-sdk';
import { Request, Response } from 'express';
@Injectable()
export class S3Service {
private readonly s3Bucket: string;
private readonly s3Region: string;
private s3: S3;

constructor(
@InjectConfig() private config: ConfigService,
) {
this.s3Bucket = config.get('s3.bucket');
this.s3Region = config.get('s3.region');
this.s3 = new S3({ region: this.s3Region, signatureVersion: 'v4' });
}

async signIn(req: Request, res: Response): Promise<any> {
const { objectName, contentType, path = '' } = req.query;
const objectNameChunks = objectName.split('/');
const filename = objectNameChunks[objectNameChunks.length - 1];
const mimeType = contentType;
const fileKey = `${path}/${objectName}`;
const params = {
Bucket: this.s3Bucket,
Key: fileKey,
Expires: 60,
ContentType: mimeType,
ACL: 'private',
};

res.set({ 'Access-Control-Allow-Origin': '*' });

this.s3.getSignedUrl('putObject', params, (err, data) => {
if (err) {
res.statusMessage = 'Cannot create S3 signed URL';

return res.status(500);
}

res.json({
signedUrl: data,
publicUrl: '/s3/uploads/' + fileKey,
filename,
fileKey,
});
});
}

tempRedirect(id: string, key: string, res: Response) {
const params = {
Bucket: this.s3Bucket,
Key: `NestJsAdminBoilerplate/${id}/${key}`,
};

this.s3.getSignedUrl('getObject', params, (err, url) => {
res.redirect(url);
});
}

}

Controller:

import { Controller, Get, Req, Res, Param } from '@nestjs/common';
import { Request, Response } from 'express';
import { S3Service } from './s3.service';
@Controller('s3')
export class S3Controller {
constructor(private s3service: S3Service) { }

@Get('/sign')
sign(@Req() req: Request, @Res() res: Response) {
return this.s3service.signIn(req, res);
}

@Get('/uploads/NestJsAdminBoilerplate/:id/:key')
fileRedirect(@Param('id') id: string, @Param('key') key: string, @Res() res: Response) {
return this.s3service.tempRedirect(id, key, res);
}
}

And let’s add the necessary environment variables to .env:

# ...
AWS_ACCESS_KEY_ID = <your aws access key>
AWS_SECRET_ACCESS_KEY = 'your aws secret key'
AWS_BUCKET_NAME = 'your bucket'
AWS_BUCKET_REGION = 'your region'
# ...

And use these variables in the config file for S3:

export default {
region: process.env.AWS_BUCKET_REGION,
bucket: process.env.AWS_BUCKET_NAME,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
};

And we’re done with uploading images.

Using Google Maps input

Sometimes we need to associate some location on the map with one of our entities. For this purpose, we created a Google Maps input —https://github.com/FusionWorks/react-admin-google-maps. To use it we will need to update our Address Entity. We will store the pointer on the map like a JSON object in the JSON column in our DB.

import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
BaseEntity,
} from 'typeorm';
import { CompanyEntity } from '../company/company.entity';
@Entity({ name: 'address' })
export class AddressEntity extends BaseEntity {

...

@Column({
type: 'json',
nullable: true,
comment: '{ lng: string, lat: string }',
})
geo: { lng: string, lat: string };

...}

Now we need to update the views of addresses forms:

import { GMapInput, GMapField } from '@fusionworks/ra-google-maps-input';
import { parse } from 'query-string';
...export const AddressCreate = props => {
const { location } = props;
const { companyId } = parse(location.search);
const redirect = companyId ? `/company/${companyId}/show/addresses`: false;return (
<Create {...props}>
<SimpleForm redirect={redirect}> ... <GMapInput source="geo" searchable googleKey="YOUR KEY HERE" validate={validateRequired} />
</SimpleForm>
</Create>
);
};const AddressTitle = ({ record }) => (<span>{`${record.name}`}</span>);export const AddressEdit = props => (
<Edit {...props} title={<AddressTitle />}>
<SimpleForm redirect={(basePath, id, data) =>
`/company/${data.companyId}/show/addresses`}>

... <GMapInput source="geo" searchable googleKey="YOUR KEY HERE" validate={validateRequired} />
</SimpleForm>
</Edit>
);export const AddressShow = props => (
<Show {...props}>
<TabbedShowLayout>
<Tab label="resources.summary.name"> ... <GMapField source="geo" googleKey="YOUR KEY HERE" />
</Tab>
</TabbedShowLayout>
</Show>
);...

Once we’re done with this, we are ready to rock!

Run our backend from “api” folder:

yarn run start

And run our frontend part with react-admin from “admin-ui” folder:

yarn start

Conclusion

In this article, we touched on the relationship between entities, but there are many more topics that have not been addressed but will be described in the future. I will cover them in the next articles and we’ll see if this stack survives nicely. So stay tuned to the FusionWorks:

Useful links

Source code for the article: https://github.com/FusionWorks/nestjs-crud-react-admin-boilerplate

If you want to dive deeper yourself, here is the set of links to documentation that might be useful for you:

5 Easy Steps To Improve Your Productivity

“Let’s Work Smarter, Not Harder” — FusionWorks

If you work hard and spend a lot of time on an elementary task, it does not mean that you are productive. Firstly let’s try to analyze what productivity is.

Productivity is commonly defined as a ratio between the output volume and the volume of inputs. Or, the rapport between completed tasks and the time spent while completing them. Employee productivity (sometimes referred to as workforce productivity) is an assessment of the efficiency of a worker or group of workers. Typically, the productivity of a given worker will be assessed relative to an average for other employees doing similar work.

Now, when we have an idea of the productivity concept we may think about the ways to improve it. Try the following and you will definitely increase efficiency! Let’s start working smarter instead of harder:

This article is powered by Empy.io — an all-in-one tool that helps HRs synchronize with the employees and handle all the internal requests in one single place. Try it for free!

1) Finish planning. Start acting.

It’s good to take the time to plan your actions, but you can waste too many hours just planning. Check what you have to do and don’t wait, start! If you count tens of thousands of times the activities you have to undertake, you gain nothing. Avoid friendship with procrastination, break with it! You can do it. Start the activity. A small step is still a move forward.

2) Meetings can last a maximum of 30 minutes.

Are all the meetings so important? Who counted how many hours a week he/she spends for: on the way to/from the meeting, unimportant discussions, boring people? If you have what to skip, do it. But if you are sure of the importance of all meetings, drop them on time. Half an hour is the perfect time to find out the latest news, decide and go on happily.

3) Say YES only to the important things.

Someone said that the difference between a successful man/woman and a very successful one is the ability to say NO. Every time we agree to do something for another person, we waste our time on activities that have absolutely nothing to do with our mission and goals. As a result, we are unhappy. We have not achieved anything important. We are tired — half a day has passed for nothing. And we are depressed — we do not find meaning in what we do. We can achieve more if we only get involved in what matters, we know exactly what we do when we do it and why we do it.

4) Free your mind.

Our brains are overworked. We bombard them with all kinds of information and more and more voluminous. Why do we remember what we only need for a very short time? Why were all kinds of diaries invented? When we want our computer/phone to work faster, we get out of all sorts of unnecessary applications, as well the mind works. Everything we can write — meetings, classes, topics for discussion, questions, etc. — write down instead of trying to memorize.

Organizing the biggest IT event in the most promising European country

Prolog

IT is always about communities. Such was my thinking in the far year of 2011 when I created the first Moldovan IT community on Facebook — DeveloperMD. After a half-year of online activity we decided that we have to get together offline. Said and done: in September 2011 we had the first DeveloperMD Community Offline with approximately 40 participants. One week later me and my partner created a software development company — FusionWorks — this was inspired by the outcome of the first event. Already in November, now having FusionWorks as the main partner, we’ve organized the 2nd edition. At some point, due to the growing size of the event, we realized that it should be done once a year and eventually we had 11 editions so far. In 2018 the big change took place — event was extended to two days, the name was changed to Moldova Developer Conference and another, much bigger venue, was selected. This led to the increase of number of participants — 350 — which makes MDC the biggest IT conference for developers in the country. This year #MDC19 is planned for November 2–3 and we expect around 450 awesome people to attend. So let me tell you more about what is going to happen in 2019.

Why #MDC19 is cool

Well, let’s count:

  1. It’s the biggest IT event for developers in the country.
  2. It has thoroughly selected content (speakers pass through a selection process and are trained by professionals).
  3. Each conference day is followed by amazing afterparty or wine tasting event (yep, Moldova is about wine and IT — not sure which one comes first).
  4. The entrance fee is more than affordable (thanks to our partners!).
  5. Theory is not everything, we have practical workshops from international experts!

Still not sure? Just watch this video from 2018. Like it? Then read more below 🙂

How to get onboard

Simple!

  1. Book the flight and accommodation (if you are not living here).
  2. Buy the ticket or apply as a speaker or partner.
  3. Get latest updates on our website and Facebook event page.
  4. Come to the event on November 2–3.
  5. Enjoy!

Why doing this?

Sometimes people ask: “Why are you doing this?”. The answer simple — we do it because we can. And because we love it! Having hundreds of bright eyes before you is the pleasure that can’t be denied. So we started in 2011 and are not going to stop.

Ok, let’s now learn more about the host country — Moldova — this small IT valley.

Top 3 facts about Moldova

  1. Moldova is not Maldives. Often mail delivery companies send parcels to the wrong country. Moldova has around 3.5 millions inhabitants and used to be USSR country until 1991. Official language is Romanian/Moldovan (the same language but is called differently in official documents). Also people understand and speak Russian here. Most of IT geeks speak English fluently, taxi drivers — not.
  2. Moldova has a very attractive IT sector — companies pay unique 7% tax that transforms BRUT into NET. IT sector capacity is about 10 000 specialists. Most of the companies are outsourcing ones and offer very attractive rates.
  3. The highest peak in Moldova is 428 meters high and the author of this article has never climbed that hill. Despite the fact that he has been to Everest BC, Kilimanjaro, Elbrus, Kazbek, Mitikas/Olimp and many others.

See you in the most promising IT country in Europe!

Anton Perkin, CEO at FusionWorks

Creating admin-like web applications with NestJS and React Admin. Part 1.

Introduction

In this series of articles, I will describe how to quickly bootstrap an API-based administration panel for your project using NestJS and React Admin.

In order to proceed further (in casу you are not reading just for fun), you may need to have NodeJS 10+npmyarn and MySQL installed on your computer. Also, you should have some basic knowledge of TypeScript and React.

Our project will consist of 2 part:

  • REST API, written in TypeScript
  • Admin panel, written in React

For demo purposes, we will create a simple application for managing guests list. This will include creating guests, showing list and updating guests info.

So let’s start.

Creating API

For creating API we will use NestJS framework. I enjoy NestJS because it is TypeScript based and thus allows producing better readable, better structured and less error prone backend code.

We will use NestJS CLI tool to initialize our backend:

npm i -g @nestjs/cli
nest new api
cd api

Now when the project skeleton is ready we will add other dependencies we need.

We will use TypeORM (again TypeScript) for working with MySQL:

yarn add @nestjs/typeorm typeorm mysql class-validator class-transformer

NestJS CRUD library to simplify our endpoints creation:

yarn add @nestjsx/crud

And NestJS Config for managing our application configuration:

yarn add nestjs-config

Once done with dependencies lets generate the skeleton of our Guests API endpoint

nest generate module guests
nest generate controller guests
nest generate service guests

And create a model class for our guest entity (src/guests/guest.entity.ts)

import { Entity, Column, PrimaryGeneratedColumn } from ‘typeorm’;
import { IsEmail } from ‘class-validator’;

@Entity({ name: 'guests' })
export class GuestEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;

@Column()
lastName: string;

@Column({
unique: true,
})
@IsEmail()
email: string;

@Column({ default: false })
isPresent: boolean;
}

Now we add some code to wire parts together

Update src/guests/guests.module.ts with TypeORM dependencies

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GuestsController } from './guests.controller';
import { GuestEntity } from './guest.entity';
import { GuestsService } from './guests.service';
@Module({
imports: [
TypeOrmModule.forFeature([GuestEntity]),
],
controllers: [
GuestsController,
],
providers: [
GuestsService,
],
})
export class GuestsModule { }

Make src/guests/guests.service.ts extend RepositoryService from NestJS CRUD

import { Injectable } from '@nestjs/common';
import { GuestEntity } from './guest.entity';
import { RepositoryService } from '@nestjsx/crud/typeorm';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class GuestsService extends RepositoryService<GuestEntity> {
constructor(@InjectRepository(GuestEntity) repository) {
super(repository);
}

}

and add it to src/guests/guests.controller.ts. Also, add Crud decorator to the controller in order to enable NestJS CRUD API-related features.

import { Controller } from '@nestjs/common';
import { Crud } from '@nestjsx/crud';
import { GuestsService } from './guests.service';
import { GuestEntity } from './guest.entity';@Crud(GuestEntity)

@Controller('guests')
export class GuestsController {
constructor(public service: GuestsService) { }
}

Finally, we configure TypeORM in the main module (src/app.module.ts):

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from 'nestjs-config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GuestsModule } from './guests/guests.module';
import * as path from 'path';@Module({
imports: [
ConfigModule.load(path.resolve(__dirname, 'config', '*.{ts,js}')),
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('database'),
inject: [ConfigService],
}),
GuestsModule,
],
controllers: [
AppController,
],
providers: [
AppService,
],
})
export class AppModule { }

And add the appropriate configuration files

src/config/database.ts:

export default {
host: process.env.DB_HOST,
type: 'mysql',
port: process.env.DB_PORT || 3306,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: ['src/**/*.entity{.ts,.js}'],
synchronize: process.env.DB_SYNCRONIZE === 'true',
logging: process.env.DB_LOGGING === 'true',
};

.env:

DB_HOST = localhost
DB_PORT = 3306
DB_USER =
DB_PASSWORD =
DB_DATABASE =
DB_SYNCRONIZE = true
DB_LOGGING = true

And we are ready to start!

Not so fast 🙂 Later on React Admin will require CORS to be enabled on API side. So we need to modify src/main.ts. And by the way lets make React’s life easier and free 3000 port!

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
await app.listen(3001);
}
bootstrap();

Now it is really ready!

yarn start

and continue to creating out administration panel UI.

Creating Admin UI

As mentioned before for this purpose we will use React Admin — a React based component library for creating admin-like interfaces.

Let’s start with initializing a React applicationn

pm install -g create-react-app
create-react-app admin-ui
cd admin-ui

Then add React Admin to the project

yarn add react-admin prop-types

and a small library developed by us (FusionWorks): @FusionWorks/ra-data-nest-crud, which integrates React Admin with our NestJS CRUD-based backend.

yarn add @fusionworks/ra-data-nest-crud

After this we are redy to initializing React Admin component and create guest editor. First, we update src/App.js with root Admin component and Resource component for guests:

import React from 'react';
import { Admin, Resource, ShowGuesser, ListGuesser } from 'react-admin';
import crudProvider from '@fusionworks/ra-data-nest-crud';
import { GuestCreate, GuestEdit } from './Guests';const dataProvider = crudProvider('http://localhost:3001');
const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="guests" list={ListGuesser} create={GuestCreate} edit={GuestEdit} show={ShowGuesser} />
</Admin>
);
export default App;

Then add corresponding forms to src/Guests/index.js.

Please note that we are using React Admin’s ListGuesser and ShowGuesser for list and show veiws. If needed they could be replaced with custom implementation same way as create and edit forms below.

import React from 'react';
import {
Create,
SimpleForm,
TextInput,
BooleanInput,
Edit,
Filter,
required,
email,
} from 'react-admin';const validateEmail = [required(), email()];
const validateRequired = required();export const GuestCreate = props => (
<Create {...props}>
<SimpleForm redirect="show">
<TextInput source="firstName" validate={validateRequired} />
<TextInput source="lastName" validate={validateRequired} />
<TextInput source="email" validate={validateEmail} />
</SimpleForm>
</Create>
);const GuestEditTitle = ({ record }) => (<span>{`${record.firstName} ${record.lastName}`}</span>);export const GuestEdit = props => (
<Edit {...props} title={<GuestEditTitle />}>
<SimpleForm redirect="list">
<TextInput source="firstName" validate={validateRequired} />
<TextInput source="lastName" validate={validateRequired} />
<TextInput source="email" validate={validateEmail} />
<BooleanInput source="isPresent" />
</SimpleForm>
</Edit>
);

Once done with this, we are ready to rock!

yarn start

Conclusion

So far everything looks great, but we have not yet touched such things as authentication, authorization, cases when database and API models should differ, etc. I will cover them in the next articles and we’ll if this stack survives nicely. So stay tuned to the FusionWorks:

Useful links

Source code for the article: https://github.com/FusionWorks/nestjs-crud-react-admin-boilerplate

If you want to dive dipper yourself here is the set of links to documentation that might be useful for you: