DEV Community

Cover image for The Complete Modern React Developer 2022
Andrew Baisden
Andrew Baisden

Posted on • Edited on

The Complete Modern React Developer 2022

Introduction

This will be a course for becoming a Complete Modern React Developer in 2022. The only three topics which are not covered in this course are Redux, GraphQL and React Native which could be covered in a future course. TypeScript is going to be the main programming language covered however if you already know JavaScript then you should find it quite easy to understand because the syntax is not that much different.

We will be building a super basic Twitter clone that has CRUD functionality for posting, reading and deleting tweets.

This course will give you the skills and knowledge to become a Software Developer across the full stack. In this course you will learn:

Prerequisites

Ensure that you have your development environment setup and install all of the tools/libraries listed in the introduction. I work on a Mac so some of the tools I mention will be macOS only however you should be able to find alternatives and be able to follow along if you use Windows or Linux.

MongoDB Setup

You will need to install the following tools for working with MongoDB NoSQL databases. MongoDB Compass is a GUI for working with MongoDB databases. And mongosh is a MongoDB shell for working with MongoDB databases using the command line.

MongoDB Compass
mongosh

PostgreSQL Setup

You will need to install the following tools for working with PostgreSQL databases. Postgres.app is an app for managing PostgreSQL databases. And Pgcli is a command line interface for Postgres which comes with auto-completion and syntax highlighting. It is the PostgreSQL equivalent of mongosh.

My preference is Valentina Studio when it comes to using a GUI for working with PostgreSQL databases. It's a great tool because it can even connect to MongoDB and MySQL databases. There are alternatives though like PgAdmin so just use whatever you feel comfortable with.

Postgres.app
Pgcli
Valentina Studio

Setting up the backend

In this section you will learn how to setup a Node backend using both Express.js and Nest.js. Both frameworks will connect to a MongoDB and PostgreSQL database using different endpoints. And as a bonus you will also learn some DevOps when we put a MongoDB and PostgreSQL database inside of a Docker container.

Docker basically gives developers the ability to package applications inside of containers. So essentially you can just have a database inside of a Docker container which any external application will be able to connect to. With this type of setup you don't even need to install or setup a database on your local machine. You can just have everything running inside of a Docker container and this setup will run exactly the same on anyones machine.

I think this a great alternative to having a local installation and with this knowledge it gives you another option for interacting with databases. This workflow does not require a huge setup and you can use either the GUI or command line to interact with your database inside of the Docker container the same way you would if it was local or online.

Local Database Setup

Pgcli Commands
mongosh Commands

MongoDB Local

Open your command line tool I will be using Hyper and run the command below to connect to your local MongoDB installation.

mongosh
Enter fullscreen mode Exit fullscreen mode

Firstly run this command which will show you which database you are using. It should return test which is the default database.

db
Enter fullscreen mode Exit fullscreen mode

Now run the command below which will show you which databases you currently have created.

show dbs;
Enter fullscreen mode Exit fullscreen mode

Next run the command to create a database called twitter.

use twitter;
Enter fullscreen mode Exit fullscreen mode

Finally create a collection using the command below and when you use the command show dbs; again in the command line you should see the database called twitter which you created.

db.createCollection('contents');
Enter fullscreen mode Exit fullscreen mode

Lastly we will add some starting data copy and paste the code below into your command line. If you run this command db.contents.find().pretty() in your command line after you have inserted the data then you will be able to see the data in the table.

db.contents.insertMany([ {tweet: "Hello World!", img: ""}, {tweet: "Content creation and programming are basically full time jobs. I have enough projects and work to keep me busy for years. Working in tech is definitely going to entertain you for a long time which is why so many people want to transition into this field.", img: ""}, {tweet: "JavaScript developers are forever in high demand", img: ""} ])
Enter fullscreen mode Exit fullscreen mode

If you open the MongoDB Compass application on your computer and connect to your local installation using the connection string mongodb://localhost:27017 then you should be able to see all of your databases including the one we just created in the GUI. So now you are setup to use either the command line or the GUI for interacting with your databases.

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650544114/mongodb-compass-twitter_hrnwiy.jpg

PostgreSQL Local

Before you do anything check that your PostgreSQL database is running locally. If it's not running then you might get an error in the command line when you run the command pgcli. On macOS I will be using the Postgres.app so get it running on your machine and then it should show up in your menu bar at the top of your operating system.

Now go to the command line and run the command below to connect to your local PostgreSQL installation.

pgcli
Enter fullscreen mode Exit fullscreen mode

Running the command below will show you all of your PostgreSQL databases.

\l
Enter fullscreen mode Exit fullscreen mode

Copy and paste the SQL query below into your pgcli command line window to create a database called twitter. Now if you run the command \l again in that same window you should see all of the databases including the one we just created.

CREATE DATABASE twitter;
Enter fullscreen mode Exit fullscreen mode

Next we need to connect to the database in the same window so use the command below to do that.

\c twitter
Enter fullscreen mode Exit fullscreen mode

Next we have to create a table and add some data which will go inside of the database twitter. Copy and paste the SQL code below into your command line window.

CREATE TABLE contents (

id UUID DEFAULT gen_random_uuid (),

tweet VARCHAR(280) NOT NULL,

img VARCHAR(500) NOT NULL

);

INSERT INTO contents (tweet, img)

VALUES ('Hello World!', ''), ('Content creation and programming are basically full time jobs. I have enough projects and work to keep me busy for years. Working in tech is definitely going to entertain you for a long time which is why so many people want to transition into this field.', ''), ('JavaScript developers are forever in high demand', '');
Enter fullscreen mode Exit fullscreen mode

If you open the Postgres.app application on your computer you should see all of the databases including the one which we just created.

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650553228/postgresql-app-twitter_ygwp3z.jpg

And if you connect to Valentina Studio or your database GUI of choice you should be able to see the database you created.

PostgreSQL database connection settings

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650553476/valentina-db-twitter-connection_ktkq6v.jpg

Valentina Studio

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650553510/valentina-db-twitter_rss8fv.jpg

Docker database setup

Start the Docker application on your computer and follow the steps below for each database. Firstly create a folder on your local machine called complete-react-developer
and then cd into using the command line.

MongoDB Docker

Double check that you are inside of the root folder for complete-react-developer and then run the commands below to setup the project.

mkdir docker-twitter-mongodb
cd docker-twitter-mongodb
touch docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Open that folder in your code editor and add the following code to the docker-compose.yml file.

docker-compose.yml

Be careful with the yaml code formatting if the indentation is not correct it will give you errors.

version: '3.9'
services:
  mongo_db:
    container_name: db_container
    image: 'mongo:latest'
    restart: always
    ports:
      - '2717:27017'
    volumes:
      - 'mongo_db:/data/db'
volumes:
  mongo_db: {}
Enter fullscreen mode Exit fullscreen mode

Now run the code below to start the docker container with the MongoDB database.

docker compose up
Enter fullscreen mode Exit fullscreen mode

Assuming everything went correctly you should have a MongoDB Database running inside of a Docker Container.

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650622543/docker-mongodb_nf7ezr.jpg

Connecting to the MongoDB Database inside the Docker Container

It is now possible to connect to the local MongoDB Database and the MongoDB Docker Database simultaneously because they are both configured to run on different ports.

The local MongoDB Database is on port 27017 so use the command below for connecting to the local mongodb database.

mongosh --port 27017
Enter fullscreen mode Exit fullscreen mode

The connection string for MongoDB Compass will be the following.

mongodb://localhost:27017
Enter fullscreen mode Exit fullscreen mode

The MongoDB Docker Database is on port 2717 so use the command below for connecting to the Docker MongoDB database.

mongosh --port 2717
Enter fullscreen mode Exit fullscreen mode

The connection string for MongoDB Compass will be the following.

mongodb://localhost:2717
Enter fullscreen mode Exit fullscreen mode

So now you have two MongoDB databases for Twitter one local and one in a Docker Container. Let's add some data to the database which is also going to persist even if you delete the container.

In the command line open a mongosh connection to the MongoDB Database inside of the Docker container.

mongosh --port 2717
Enter fullscreen mode Exit fullscreen mode

Run the commands below. You are creating a database called twitter with a collection called contents. And then you are inserting some data into the database.

use twitter;

db.createCollection('contents');

db.contents.insertMany([ {tweet: "Hello World!", img: ""}, {tweet: "Content creation and programming are basically full time jobs. I have enough projects and work to keep me busy for years. Working in tech is definitely going to entertain you for a long time which is why so many people want to transition into this field.", img: ""}, {tweet: "JavaScript developers are forever in high demand", img: ""} ]);
Enter fullscreen mode Exit fullscreen mode

Now when you run the command db.contents.find().pretty(); inside of the command line it should return the data you just inserted. If you go to MongoDB compass and use this connection string mongodb://localhost:2717 you should see the twitter database with the data inside.

PostgreSQL Docker

Check that you are inside of the root folder for complete-react-developer and then run the commands below to setup the project.

mkdir docker-twitter-postgresql
cd docker-twitter-postgresql
touch docker-compose.yml
mkdir sql
cd sql
touch twitter.sql
cd ..
Enter fullscreen mode Exit fullscreen mode

Open that folder in you code editor and add the following code to the docker-compose.yml and twitter.sql files.

docker-compose.yml

Be careful with the yaml code formatting if the indentation is not correct it will give you errors.

version: '3.7'
services:
  postgres:
    image: postgres:latest
    restart: always
    environment:
      - POSTGRES_USER=twitter
      - POSTGRES_PASSWORD=twitter
      - POSTGRES_DB=twitter
    ports:
      - '5433:5432'
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
      # copy the sql script to create tables
      - ./sql/twitter.sql:/docker-entrypoint-initdb.d/twitter.sql
Enter fullscreen mode Exit fullscreen mode

twitter.sql

CREATE TABLE contents (

id UUID DEFAULT gen_random_uuid (),

tweet VARCHAR(280) NOT NULL,

img VARCHAR(500) NOT NULL

);

INSERT INTO contents (tweet, img)

VALUES ('Hello World!', ''), ('Content creation and programming are basically full time jobs. I have enough projects and work to keep me busy for years. Working in tech is definitely going to entertain you for a long time which is why so many people want to transition into this field.', ''), ('JavaScript developers are forever in high demand', '');
Enter fullscreen mode Exit fullscreen mode

Now run the code below to start the Docker Container with the PostgreSQL database.

docker compose up
Enter fullscreen mode Exit fullscreen mode

When you see the log that says database system is ready to accept connections you will know that it's working. This can also be verified in the Docker Desktop Application if you check it you should see the container running.

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650557620/docker-postgresql_z3nl5i.jpg

What we have just done is setup a PostgreSQL database which will reside inside of a Docker container. This database is even going to have some prebuilt data from the SQL script that we created.

Connecting to the PostgreSQL Database inside the Docker Container

The docker-compose.yml file inside of the docker-twitter-postgresql folder has a port mapping of 5433:5432. 5433 is the local port and 5432 is the docker port which is also the default for Postgres. I did it this way so that we can use the Postgres app on port 5432 locally and run the Docker database on port 5433 simultaneously.

So here is where the magic happens! Use the connection credentials in the image below and you should be able to connect to the PostgreSQL database inside of the docker container!

The password is twitter by the way and you can find the credentials inside of the docker-compose.yml file.

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650558031/docker-postgresql-db-connection_xajz8z.jpg

Alternatively, you can also use pgcli to connect to the database in the command line by using the command below. The password is twitter like earlier.

pgcli -h localhost -p 5433 -u twitter -d twitter
Enter fullscreen mode Exit fullscreen mode

So now we have a local PostgreSQL database called twitter on port 5432. And a Docker PostgreSQL database called twitter on port 5433. Valentina Studio can connect to both of them simultaneously and you can run all of your SQL queries. Whats more the PostgreSQL database inside of the Docker container can persist its data. If you were to delete the running Docker container and then run the command docker compose up again everything will remain the same!

Use the command docker compose down to stop the Docker container from running if you need to.

Congratulations you just learnt the basics of MongoDB, PostgreSQL and Docker!

Node backend setup

This will be split into two sections. One for creating a backend using Express.js and TypeScript. And another for creating a backend using Nest.js and TypeScript so you can see the difference between the two and learn alternative ways for developing a backend in Node.

So there will be 4 Node backends to play around with:

  • backend-express-mongodb
  • backend-express-postgresql
  • backend-nest-mongodb
  • backend-nest-postgresql

A REST API testing tool will be needed so that you can test the various routes and endpoints. These are my top 3 preferences feel free to use something else if you want to.

Postman
Thunder Client
Insomnia

You don't need to create all of these backends because when we create the React frontend it will only require one backend but this is still good knowledge worth learning. And obviously you could just have one backend that connects to both MongoDB and PostgreSQL it's just easier to explain in these examples.

Express App

Backend Express MongoDB

Be certain that you are inside of the folder complete-react-developer.

Run the commands below to scaffold your project.

mkdir backend-express-mongodb
cd backend-express-mongodb
tsc --init
npm init -y
npm i express @types/express @types/node ts-node cors @types/cors mongoose @types/mongoose typescript rimraf copy-files dotenv nodemon
touch .env
mkdir src
cd src
touch app.ts
mkdir controllers models routes
touch controllers/Admin.ts
touch models/Twitter.ts
touch routes/Admin.ts
cd ..
Enter fullscreen mode Exit fullscreen mode

Open the project in your code editor and go to the tsconfig.json file inside of the root folder and enable these properties.

"rootDir": "./src" /* Specify the root folder within your source files. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"outDir": "./dist/src" /* Specify an output folder for all emitted files. */,
Enter fullscreen mode Exit fullscreen mode

In the next step open the package.json file and add these run scripts.

"scripts": {

"start": "node dist/src/app.js",

"dev": "nodemon src/app.ts",

"clean": "rimraf dist/",

"build": "npm run clean && tsc && npm run copy-files",

"copy-files": "copyfiles -u 1 src/**/*.html src/**/*.css src/**/*.json dist/src"

},
Enter fullscreen mode Exit fullscreen mode
Run Scripts

start
The start script runs the application using Node without auto reload when there are updates to the files.

dev
The dev script uses nodemon to automatically auto reload and update the files when there are changes.

clean
The clean script deletes the dist folder.

build
The build script deletes the dist folder and then automatically copies all of the files and puts them back into the dist folder.

copy-files
The copy files script is used for copying the files from one directory to a different one.

Adding the code

Lastly add the code below to their corresponding files.

controllers/Admin.ts

import { Response, Request } from 'express';

import mongoose from 'mongoose';

import Twitter from '../models/Twitter';

export const getTweets = (req: Request, res: Response): void => {
    Twitter.find((err, data) => {
        console.log(data);

        res.json(data);

        if (err) {
            console.log(err);
        }
    });
};

export const getTweet = async (req: Request, res: Response): Promise<any> => {
    const tweetId = req.params.tweetId;

    console.log('Tweet ID', tweetId);

    // This line of code fixes the CastError: Cast to ObjectId failed for value "favicon.ico" (type string) at path "_id" for model "contents"

    if (!mongoose.Types.ObjectId.isValid(tweetId)) return false;

    await Twitter.findById(tweetId).exec();

    Twitter.findById(tweetId, (err: any, tweet: any) => {
        console.log(tweet);

        res.json(tweet);

        if (err) {
            console.log(err);
        }
    });
};

export const postTweet = (req: Request, res: Response) => {
    const { tweet, img } = req.body;

    const twitter = new Twitter({ tweet: tweet, img: img });

    twitter.save();

    console.log('Tweet Created');

    res.status(201).json({ msg: 'Tweet Created' });
};

export const updateTweet = (req: Request, res: Response) => {
    const tweetId = req.params.tweetId;

    const { tweet, img } = req.body;

    Twitter.findByIdAndUpdate(tweetId, { tweet: tweet, img: img }).then(() => {
        console.log(`Tweet ${tweetId} Updated`);

        res.json({ msg: `Tweet ${tweetId} Updated` });
    });
};

export const deleteTweet = (req: Request, res: Response) => {
    const tweetId = req.body.tweetId;

    Twitter.findByIdAndRemove(tweetId, () => {
        res.json({ msg: `Tweet ${tweetId} Deleted` });
    });
};
Enter fullscreen mode Exit fullscreen mode

models/Twitter.ts

import { Schema, model } from 'mongoose';

interface Twitter {
    tweet: string;

    img: string;
}

const schema = new Schema<Twitter>({
    tweet: { type: String, required: true },

    img: { type: String, required: false },
});

const TwitterModel = model<Twitter>('contents', schema);

export default TwitterModel;
Enter fullscreen mode Exit fullscreen mode

routes/Admin.ts

import express from 'express';

import { getTweets, getTweet, postTweet, updateTweet, deleteTweet } from '../controllers/Admin';

const router = express.Router();

router.get('/', getTweets);

router.get('/:tweetId', getTweet);

router.post('/delete', deleteTweet);

router.post('/tweet', postTweet);

router.post('/:tweetId', updateTweet);

export default router;
Enter fullscreen mode Exit fullscreen mode

app.ts

import dotenv from 'dotenv';

dotenv.config();

console.log(process.env.DB_HOST);

import express from 'express';

import cors from 'cors';

import mongoose from 'mongoose';

import adminRoute from './routes/Admin';

const app = express();

app.use(cors());

app.use(express.urlencoded({ extended: false }));

app.use(express.json());

app.use('/', adminRoute);

const port = process.env.PORT || 8080;

mongoose

    // Use DB_HOST_DOCKER to connect to the MongoDB Database in the Docker Container

    .connect(`${process.env.DB_HOST_LOCAL}`)

    .then(() => {
        app.listen(port, () => console.log(`Server and database running on port ${port}, http://localhost:${port}`));
    })

    .catch((err: any) => {
        console.log(err);
    });
Enter fullscreen mode Exit fullscreen mode

.env

DB_HOST_LOCAL="mongodb://127.0.0.1:27017/twitter"

DB_HOST_DOCKER="mongodb://127.0.0.1:2717/twitter"
Enter fullscreen mode Exit fullscreen mode

The application is setup to connect to a local MongoDB database but you can change this in the app.ts file and you can find the database connection strings in the .env file.

Use the command below to start the server.

npm run dev
Enter fullscreen mode Exit fullscreen mode
Testing the API

I will be using Postman but you can use whatever API Testing tool you want. If you want to see it fully working the first thing you will have to do is add some data to the database if you have not done so already. Use the Create tweet route for this and see the examples in the screenshots below.

GET all tweets

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650806989/postman-get-tweets_osgha3.jpg

GET tweet by ID

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650807143/postman-get-tweet_lc03qt.jpg

CREATE tweet

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650807209/postman-post-tweet_p7fycd.jpg

UPDATE tweet by ID

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650807250/postman-update-tweet_vvookc.jpg

DELETE tweet

https://res.cloudinary.com/d74fh3kw/image/upload/c_scale,w_800/v1650807306/postman-delete-tweet_mp4bs1.jpg

Backend Express PostgreSQL

We will be using https://typeorm.io/to create an Express.js application that connects to a PostgreSQL database.

You should be inside of the folder complete-react-developer.

Run the commands below to scaffold your project.

mkdir backend-express-postgresql
cd backend-express-postgresql
tsc --init
npm init -y
npm i express @types/express @types/node ts-node cors @types/cors typescript rimraf copy-files dotenv nodemon pg reflect-metadata typeorm
mkdir src
cd src
touch app.ts app-data-source.ts
mkdir entity
cd entity
touch Tweet.ts
cd ../..
Enter fullscreen mode Exit fullscreen mode

Open the project in your code editor find the tsconfig.json file and replace all of the code inside it with the code below.

{
    "compilerOptions": {
        "lib": ["es5", "es6", "dom"],

        "target": "es5",

        "module": "commonjs",

        "moduleResolution": "node",

        "emitDecoratorMetadata": true,

        "experimentalDecorators": true,

        "rootDir": "./src",

        "outDir": "./dist/src"
    }
}
Enter fullscreen mode Exit fullscreen mode

In the next step open the package.json file and add these run scripts.

"scripts": {

"start": "node dist/src/app.js",

"dev": "nodemon src/app.ts",

"clean": "rimraf dist/",

"build": "npm run clean && tsc && npm run copy-files",

"copy-files": "copyfiles -u 1 src/**/*.html src/**/*.css src/**/*.json dist/src"

},
Enter fullscreen mode Exit fullscreen mode

Add the code below to the corresponding files.

entity/Tweet.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Tweet {
    @PrimaryGeneratedColumn('uuid')
    id: string;

    @Column()
    tweet: string;

    @Column()
    img: string;
}
Enter fullscreen mode Exit fullscreen mode

app-data-source.ts

import { DataSource } from 'typeorm';

export const myDataSource = new DataSource({
    type: 'postgres',

    host: 'localhost',

    port: 5432,

    username: 'postgres',

    password: '',

    database: 'twitter',

    entities: ['dist/src/entity/*.js'],

    logging: true,

    synchronize: true,
});
Enter fullscreen mode Exit fullscreen mode

app.ts

import * as express from 'express';

import { Request, Response } from 'express';

import { Tweet } from './entity/Tweet';

import { myDataSource } from './app-data-source';

// establish database connection

myDataSource

    .initialize()

    .then(() => {
        console.log('Data Source has been initialized!');
    })

    .catch((err) => {
        console.error('Error during Data Source initialization:', err);
    });

// create and setup express app

const app = express();

app.use(express.json());

app.use(express.urlencoded({ extended: false }));

// register CRUD routes

// CREATE

// READ

// UPDATE

// DELETE

// READ: All tweets

app.get('/tweets', async function (req: Request, res: Response) {
    const tweets = await myDataSource.getRepository(Tweet).find();

    res.json(tweets);
});

// READ: Tweet by ID

app.get('/tweets/:id', async function (req: Request, res: Response) {
    const results = await myDataSource.getRepository(Tweet).findOneBy({
        id: req.params.id,
    });

    return res.send(results);
});

// CREATE: New tweet

app.post('/tweets', async function (req: Request, res: Response) {
    const tweet = await myDataSource.getRepository(Tweet).create(req.body);

    const results = await myDataSource.getRepository(Tweet).save(tweet);

    return res.send(results);
});

// UPDATE: Tweet by ID

app.put('/tweets/:id', async function (req: Request, res: Response) {
    const tweet = await myDataSource.getRepository(Tweet).findOneBy({
        id: req.body.id,
    });

    myDataSource.getRepository(Tweet).merge(tweet, req.body);

    const results = await myDataSource.getRepository(Tweet).save(tweet);

    return res.send(results);
});

// DELETE: Tweet by ID

app.delete('/tweets/:id', async function (req: Request, res: Response) {
    const results = await myDataSource.getRepository(Tweet).delete(req.body.id);

    return res.send(results);
});

const port = process.env.PORT || 8080;

// start express server

app.listen(port, () => console.log(`Server and database running on port ${port}, http://localhost:${port}`));
Enter fullscreen mode Exit fullscreen mode

The application is setup to connect to a local PostgreSQL database but you can change this in the app-data-source.ts file. The Docker connection settings can be found in the Docker section if you need them. Don't forget that you need to have your PostgreSQL database setup and running before you can connect to it.

Use the commands below to run the app.

Warning: You might get the error EntityMetadataNotFoundError: No metadata for "Tweet" was found. if you try to use the command npm run dev which uses nodemon to start the application. I think it has something to do with static and dynamic data and the fact that nodemon is auto reloading. So it's safer to use the commands below and just do a clean build using the node server which won't update until you manually restart it.

npm run build
npm run start
Enter fullscreen mode Exit fullscreen mode

You should be familiar with using a REST API tool now however the routes and CRUD requests are slightly different in this example. See the examples below and don't forget to use the CREATE tweet route to add some data into the database so that you can see some data.

GET all tweets

Request: GET
Route: http://localhost:8080/tweets

GET tweet by ID

Request: GET
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1

CREATE tweet

Request: POST
Route: http://localhost:8080/tweets
Body raw: {"tweet": 'Hello World', img: ""}

UPDATE tweet by ID

Request: PUT
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1
Body raw: {"tweet": 'Hello Moon', img: ""}

DELETE tweet by ID

Request: DELETE
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1
Body: x-www-form-urlencoded
KEY: id
VALUE: Your id

Nest App

Backend Nest MongoDB

It's time to create the Nest backend that will connect to MongoDB. Get inside of the folder complete-react-developer and run the command below. Select your preferred package manager I am going to choose npm. If you choose a different option remember to run the correct commands for it later on.

nest new backend-nest-mongodb
Enter fullscreen mode Exit fullscreen mode

Open the project in your code editor and get ready to generate some controller and service files. We will also install mongoose first cd into the backend-nest-mongodb folder in the command line and run the commands below.

cd backend-nest-mongodb
npm install --save @nestjs/mongoose mongoose
nest g controller twitter
nest g service twitter
Enter fullscreen mode Exit fullscreen mode

Before we create the other project files lets do some file cleanup. Delete the following files:

app.service.ts
app.controller.ts
app.controller.spec.ts

Now it's time to create the rest of the files for this project. Get inside of the root folder for backend-nest-mongodb and run the commands below.

touch src/twitter/twitter.module.ts
mkdir src/twitter/{dto,schemas}
touch src/twitter/dto/create-twitter.dto.ts
touch src/twitter/schemas/twitter.schema.ts
Enter fullscreen mode Exit fullscreen mode

We have created all of the files that are going to be required for this project let's add the code now. Add or replace the code in the existing files with the code below:

app.module.ts

import { Module } from '@nestjs/common';

import { MongooseModule } from '@nestjs/mongoose';

import { TwitterController } from './twitter/twitter.controller';

import { TwitterService } from './twitter/twitter.service';

import { TwitterModule } from './twitter/twitter.module';

@Module({
    imports: [
        TwitterModule,

        // Local MongoDb database

        // Change the port to 127.0.0.1:2717 to connect to Docker

        MongooseModule.forRoot('mongodb://127.0.0.1:27017/twitter'),
    ],

    controllers: [TwitterController],

    providers: [TwitterService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

twitter.service.ts

import { Model } from 'mongoose';

import { Injectable } from '@nestjs/common';

import { InjectModel } from '@nestjs/mongoose';

import { Twitter, TwitterDocument } from './schemas/twitter.schema';

import { CreateTwitterDto } from './dto/create-twitter.dto';

@Injectable()
export class TwitterService {
    constructor(@InjectModel(Twitter.name) private twitterModel: Model<TwitterDocument>) {}

    async create(createTwitterDto: CreateTwitterDto): Promise<Twitter> {
        const createdTwitter = new this.twitterModel(createTwitterDto);

        return createdTwitter.save();
    }

    async findAll(): Promise<Twitter[]> {
        return this.twitterModel.find().exec();
    }

    async findOne(id: string): Promise<Twitter> {
        return this.twitterModel.findOne({ _id: id });
    }

    async update(id: string, twitter: Twitter): Promise<Twitter> {
        return this.twitterModel.findByIdAndUpdate(id, twitter, { new: true });
    }

    async delete(id: string): Promise<Twitter> {
        return this.twitterModel.findByIdAndRemove({ _id: id });
    }
}
Enter fullscreen mode Exit fullscreen mode

twitter.module.ts

import { Module } from '@nestjs/common';

import { MongooseModule } from '@nestjs/mongoose';

import { Twitter, TwitterSchema } from './schemas/twitter.schema';

@Module({
    imports: [MongooseModule.forFeature([{ name: Twitter.name, schema: TwitterSchema }])],

    exports: [MongooseModule],
})
export class TwitterModule {}
Enter fullscreen mode Exit fullscreen mode

twitter.controller.ts

import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';

import { CreateTwitterDto, TwitterDto } from './dto/create-twitter.dto';

import { TwitterService } from './twitter.service';

@Controller('tweets')
export class TwitterController {
    constructor(private twitterService: TwitterService) {}

    @Post()
    async create(@Body() createTwitterDto: CreateTwitterDto) {
        this.twitterService.create(createTwitterDto);
    }

    @Get()
    async findAll(): Promise<TwitterDto[]> {
        return this.twitterService.findAll();
    }

    @Get(':id')
    async findOne(@Param('id') id): Promise<TwitterDto> {
        return this.twitterService.findOne(id);
    }

    @Put(':id')
    update(
        @Body() updateTwitterDto: CreateTwitterDto,

        @Param('id') id
    ): Promise<TwitterDto> {
        return this.twitterService.update(id, updateTwitterDto);
    }

    @Delete(':id')
    delete(@Param('id') id): Promise<TwitterDto> {
        return this.twitterService.delete(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

schemas/twitter.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';

import { Document } from 'mongoose';

export type TwitterDocument = Twitter & Document;

@Schema()
export class Twitter {
    @Prop()
    tweet: string;

    @Prop()
    img: string;
}

export const TwitterSchema = SchemaFactory.createForClass(Twitter);
Enter fullscreen mode Exit fullscreen mode

dto/create-twitter.dto.ts

export class CreateTwitterDto {
    id?: string;

    tweet: string;

    img: string;
}

export class TwitterDto {
    id?: string;

    tweet: string;

    img: string;
}
Enter fullscreen mode Exit fullscreen mode

Everything should be setup now the backend is configured to connect to a local MongoDB database. You can change this to Docker by editing the connection string inside of the app.module.ts file.

Run the command below to start the application in watch mode.

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

One important thing to mention is that by default NestJS applications run on port 3000 which is the same default port that our React application is going to use. So for consistency you might want to change it to 8080 or a different port. You can do this in the main.ts file. Also you will need to enable CORS otherwise you will get that annoying CORS error when you try to connect the backend to the frontend.

main.ts

import { NestFactory } from '@nestjs/core';

import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);

    app.enableCors();

    await app.listen(8080);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

The routes should be the same as before here is a refresher that you can test in Postman or whatever REST API tool you are using:

GET all tweets

Request: GET
Route: http://localhost:8080/tweets

GET tweet by ID

Request: GET
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1

CREATE tweet

Request: POST
Route: http://localhost:8080/tweets
Body raw: {"tweet": 'Hello World', img: ""}

UPDATE tweet by ID

Request: PUT
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1
Body raw: {"tweet": 'Hello Moon', img: ""}

DELETE tweet by ID

Request: DELETE
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1

Backend Nest PostgreSQL

Lastly we are now going to create the Nest backend that will connect to PostgreSQL. We will finally move onto the React frontend after this stage. Be sure that you are inside of the folder complete-react-developer and run the command below. Like in the previous chapter select your preferred package manager I am going to choose npm. If you choose a different option remember to run the correct commands for it later on.

nest new backend-nest-postgresql
Enter fullscreen mode Exit fullscreen mode

Open the project in your code editor and get ready to generate some controller and service files. We will also install PostgreSQL and TypeORM so that we can connect to PostgreSQL databases. Firstly cd into the backend-nest-postgresql folder in the command line and run the commands below.

cd backend-nest-postgresql
npm install --save pg @nestjs/typeorm typeorm
nest g controller twitter
nest g service twitter
Enter fullscreen mode Exit fullscreen mode

Before we create the other project files lets do some file cleanup. Delete the following files:

app.service.ts
app.controller.ts
app.controller.spec.ts

Now it's time to create the rest of the files for this project. When you are inside of the root folder for backend-nest-postgresql run the commands below.

touch src/twitter/{twitter.module.ts,twitter.entity.ts}
mkdir src/twitter/dto
touch src/twitter/dto/twitter.dto.ts
Enter fullscreen mode Exit fullscreen mode

We have created all of the files that are going to be required for this project let's add the code now. Add or replace the code in the existing files with the code below:

app.module.ts

import { Module } from '@nestjs/common';

import { TypeOrmModule } from '@nestjs/typeorm';

import { TwitterController } from './twitter/twitter.controller';

import { TwitterService } from './twitter/twitter.service';

import { TwitterModule } from './twitter/twitter.module';

import { Connection } from 'typeorm';

@Module({
    imports: [
        TypeOrmModule.forRoot({
            type: 'postgres',

            host: 'localhost',

            port: 5432,

            username: 'postgres',

            password: '',

            database: 'twitter',

            entities: ['dist/**/*.entity{.ts,.js}'],

            synchronize: false,
        }),

        TwitterModule,
    ],

    controllers: [TwitterController],

    providers: [TwitterService],
})
export class AppModule {
    constructor(private connection: Connection) {}
}
Enter fullscreen mode Exit fullscreen mode

twitter.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';

import { InjectRepository } from '@nestjs/typeorm';

import { DeleteResult, InsertResult, Repository } from 'typeorm';

import { Twitter } from './twitter.entity';

@Injectable()
export class TwitterService {
    constructor(
        @InjectRepository(Twitter)
        private twitterRepository: Repository<Twitter>
    ) {}

    async addTwitter(twitter: Twitter): Promise<InsertResult> {
        return this.twitterRepository.insert(twitter);
    }

    async findAll(): Promise<Twitter[]> {
        return this.twitterRepository.find();
    }

    async findOne(id: string): Promise<Twitter> {
        return this.twitterRepository.findOne(id);
    }

    async update(id: string, twitter: Twitter): Promise<Twitter> {
        const twitterUpdate = await this.findOne(id);

        if (twitterUpdate === undefined) {
            throw new NotFoundException();
        }

        await this.twitterRepository.update(id, twitter);

        return this.twitterRepository.findOne(id);
    }

    async delete(id: string): Promise<DeleteResult> {
        const twitterUpdate = await this.findOne(id);

        if (twitterUpdate === undefined) {
            throw new NotFoundException();
        }

        return this.twitterRepository.delete(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

twitter.module.ts

import { Module } from '@nestjs/common';

import { TypeOrmModule } from '@nestjs/typeorm';

import { TwitterController } from './twitter.controller';

import { TwitterService } from './twitter.service';

import { Twitter } from './twitter.entity';

@Module({
    imports: [TypeOrmModule.forFeature([Twitter])],

    controllers: [TwitterController],

    providers: [TwitterService],

    exports: [TypeOrmModule],
})
export class TwitterModule {}
Enter fullscreen mode Exit fullscreen mode

twitter.entity.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Twitter {
    @PrimaryGeneratedColumn('uuid')
    id: string;

    @Column()
    tweet: string;

    @Column()
    img: string;
}
Enter fullscreen mode Exit fullscreen mode

twitter.controller.ts

import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common';

import { TwitterService } from './twitter.service';

import { TwitterDto } from './dto/twitter.dto';

import { Twitter } from './twitter.entity';

@Controller('tweets')
export class TwitterController {
    constructor(private twitterService: TwitterService) {}

    @Post()
    create(@Body() twitter: Twitter) {
        return this.twitterService.addTwitter(twitter);
    }

    @Get()
    findAll(): Promise<TwitterDto[]> {
        return this.twitterService.findAll();
    }

    @Get(':id')
    getOneTwitter(@Param('id') id: string): Promise<Twitter> {
        return this.twitterService.findOne(id);
    }

    @Patch(':id')
    updateTwitter(
        @Param('id') id: string,

        @Body() twitter: Twitter
    ): Promise<Twitter> {
        return this.twitterService.update(id, twitter);
    }

    @Delete(':id')
    deleteTwitter(@Param('id') id: string) {
        return this.twitterService.delete(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

dto/twitter.dto.ts

export class TwitterDto {
    tweet: string;

    img: string;
}
Enter fullscreen mode Exit fullscreen mode

Everything should be setup now the backend is configured to connect to a local PostgreSQL database. You can change this to Docker by editing the connection details inside of the app.module.ts file.

There is one thing of note though this application is using a database table called twitter. Check out the example SQL that you could use for generating some quick test data. If you get an error its probably because its expecting to find a table called twitter.

CREATE TABLE twitter (



id UUID DEFAULT gen_random_uuid (),



tweet VARCHAR(280) NOT NULL,



img VARCHAR(500) NOT NULL



);



INSERT INTO twitter (tweet, img)



VALUES ('Hello World!', ''), ('Content creation and programming are basically full time jobs. I have enough projects and work to keep me busy for years. Working in tech is definitely going to entertain you for a long time which is why so many people want to transition into this field.', ''), ('JavaScript developers are forever in high demand', '');
Enter fullscreen mode Exit fullscreen mode

Run the command below to start the application in watch mode. Like before the routes are quite similar but there are some differences. Also don't forget to change the port to 8080 in the main.ts file. Like earlier you will need to enable CORS otherwise you will get that annoying CORS error when you try to connect the backend to the frontend.

main.ts

import { NestFactory } from '@nestjs/core';

import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);

    app.enableCors();

    await app.listen(8080);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode
npm run start:dev
Enter fullscreen mode Exit fullscreen mode

You can test in Postman or whatever REST API tool you are using:

GET all tweets

Request: GET
Route: http://localhost:8080/tweets

GET tweet by ID

Request: GET
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1

CREATE tweet

Request: POST
Route: http://localhost:8080/tweets
Body raw: {"tweet": 'Hello World', img: ""}

UPDATE tweet by ID

Request: PATCH
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1
Body raw: {"tweet": 'Hello Moon', img: ""}

DELETE tweet by ID

Request: DELETE
Route: http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1
Body: x-www-form-urlencoded
KEY: id
VALUE: Your id

Setting up the frontend

At long last we reach the frontend section! It wont be anywhere near as long as the backend section we just completed because there will only be ONE React frontend!

Building the Twitter Clone App

The application is going to be a very simple Twitter Clone. You can create, read and delete tweets. There is no option to update/edit tweets which is exactly how it is right now anyway πŸ˜‚ However the endpoint for updating already exists in the backend so you could implement it if you wanted to. BTW this is not a Twitter Clone course so don't expect it to be pixel perfect and 100% accurate 😁

The codebase is quite large so instead of copy and pasting code a dozen times and going through a long project setup I created the application and put it on GitHub. So all you need to do is clone/download the codebase and run the installation scripts.

https://github.com/andrewbaisden/complete-react-developer

Next open the project in your code editor to see the codebase and use the commands below inside of their respective root folders. The setup instructions are also in the README file.

Setup

Start the Docker Desktop Application on your computer

cd into the root folder for backend-nest-mongodb and frontend and then run the commands below to install the dependencies. You will probably need to force the installation when trying to install the dependencies for the frontend React application in this case otherwise it could give you an error.

# Run this command inside of the backend-nest-mongodb folder
npm install

# Run this command inside of the frontend folder
npm install --force
Enter fullscreen mode Exit fullscreen mode

cd into the root folder for docker-twitter-mongodb and run the command below to start the MongoDB database inside of a Docker Container.

docker compose up
Enter fullscreen mode Exit fullscreen mode

cd into the root folder for backend-nest-mongodb and run the command below to start the backend NestJS server.

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

cd into the root folder for frontend and run the command below to start the frontend React server.

npm run start
Enter fullscreen mode Exit fullscreen mode

Use the routes from the Backend Nest MongoDB section if you want to test them out in your REST API tool.

The Twitter Clone App

You should see your database running inside of a Docker Container and your Twitter Clone React application open in the browser.

Run these commands inside of the root folder for frontend which is where React is. The command below starts Storybook.

# Starts Storybook
npm run storybook
Enter fullscreen mode Exit fullscreen mode

You should see a Storybook component library open in the browser with a component for composing tweets. You can play around and change the names in the control to see how it looks in the demo. The command below runs the unit and integration tests.

# Runs the React testing library unit and integration tests
npm run test
Enter fullscreen mode Exit fullscreen mode

You might need to press a or Enter to trigger a new test run. All of the tests should be passing in your console. The command below starts Cypress.

# Runs the Cypress End-To-End tests
npx cypress open
Enter fullscreen mode Exit fullscreen mode

A new Cypress window should open. Run the integration test and get ready to be amazed as it automatically posts 3 tweets for you! Reload the web page with your React application and you will see the new tweets there too!

The Context API

This application uses the Context API for global state. If you want to get the application to connect to your MongoDB, PostgreSQL or Docker databases then you need to change the API routes and port numbers http://localhost:8080/tweets. The same applies to the methods don't forget that some of them use POST, PUT, PATCH, DELETE etc... It depends on the backend you are using.

src/contexts/TwitterContext.tsx

import { useEffect, useState, createContext, useContext } from 'react';

interface ContextProps {
    data: any;

    loading: boolean;

    handleToggleComposetweet: any;

    toggleComposeTweet: boolean;

    tweet: string;

    setTweet: any;

    postTweet: any;

    deleteTweet: any;
}

const TwitterContext = createContext({} as ContextProps);

export const useTwitter = () => useContext(TwitterContext);

const TwitterContextProvider = (props: any): any => {
    useEffect(() => {
        const getTweets = () => {
            const API = 'http://localhost:8080/tweets';

            fetch(API)
                .then((response) => {
                    console.log(response);

                    return response.json();
                })

                .then((data) => {
                    console.log(data);

                    setLoading(false);

                    setData(data);
                })

                .catch((err) => {
                    console.log(err);
                });
        };

        getTweets();
    }, []);

    const [data, setData] = useState([]);

    const [loading, setLoading] = useState(true);

    const [toggleComposeTweet, setToggleComposeTweet] = useState(false);

    const [tweet, setTweet] = useState('');

    const handleToggleComposetweet = () => {
        toggleComposeTweet === true ? setToggleComposeTweet(false) : setToggleComposeTweet(true);
    };

    const postTweet = () => {
        if (tweet === '') {
            let myHeaders = new Headers();

            myHeaders.append('Content-Type', 'application/json');

            let raw = JSON.stringify({
                tweet: 'Congratulations this is what happens when you post an empty tweet πŸ€ͺ Create some validation πŸ™ƒ',

                img: '',
            });

            fetch('http://localhost:8080/tweets', { method: 'POST', headers: myHeaders, body: raw, redirect: 'follow' })
                .then((response) => response.text())

                .then((result) => console.log(result))

                .catch((error) => console.log('error', error));
        } else {
            let myHeaders = new Headers();

            myHeaders.append('Content-Type', 'application/json');

            let raw = JSON.stringify({
                tweet: tweet,

                img: '',
            });

            fetch('http://localhost:8080/tweets', { method: 'POST', headers: myHeaders, body: raw, redirect: 'follow' })
                .then((response) => response.text())

                .then((result) => console.log(result))

                .catch((error) => console.log('error', error));
        }
    };

    const deleteTweet = (tweetId: string) => {
        console.log('Deleted', tweetId);

        let urlencoded = new URLSearchParams();

        fetch(`http://localhost:8080/tweets/${tweetId}`, {
            method: 'DELETE',

            body: urlencoded,

            redirect: 'follow',
        })
            .then((response) => response.text())

            .then((result) => console.log(result))

            .catch((error) => console.log('error', error));

        window.location.reload();
    };

    const value = {
        data,

        loading,

        toggleComposeTweet,

        handleToggleComposetweet,

        postTweet,

        tweet,

        setTweet,

        deleteTweet,
    };

    return <TwitterContext.Provider value={value}>{props.children}</TwitterContext.Provider>;
};

export default TwitterContextProvider;
Enter fullscreen mode Exit fullscreen mode

Testing with React Testing Library and Jest

There are two test files one for App.test.tsx and one for TwitterMenu.test.tsx.

I will show the example for App.test.tsx. These tests just test to see if the required text is displaying on the page. Each component should have a test file to go alongside them.

App.test.tsx

import { render, screen } from '@testing-library/react';

import App from './App';

describe('<App />', () => {
    it('has a following text label', () => {
        render(<App />);

        const el = screen.getByText(/Following/i);

        expect(el).toBeTruthy();
    });

    it('has a followers text label', () => {
        render(<App />);

        const el = screen.getByText(/Followers/i);

        expect(el).toBeTruthy();
    });

    it('has a you might like heading', () => {
        render(<App />);

        const el = screen.getByText(/You might like/i);

        expect(el.innerHTML).toBe('You might like');
    });

    it('has a whats happening heading', () => {
        render(<App />);

        const el = screen.getByText(/Whats happening/i);

        expect(el.innerHTML).toBe('Whats happening');
    });
});
Enter fullscreen mode Exit fullscreen mode

End-To-End Testing with Cypress

This Cypress test will automatically post 3 tweets. It's all done in real time and the tweets will show up in your database and on the live application.

cypress/integratioin/tweet.spec.js

describe('user form flow', () => {
    beforeEach(() => {
        cy.viewport(1600, 900);

        cy.visit('http://localhost:3000/');
    });

    it('user posts a tweet', () => {
        // Post a tweet

        cy.get('.compose-tweet-btn').click();

        cy.get('textarea[name="tweet"]').type(
            'What happened to all that fun you were having?! Come on, lets try to enjoy this!'
        );

        cy.wait(3000);

        cy.get('.post-tweet-btn').click();
    });

    it('user posts a second tweet', () => {
        // Post a tweet

        cy.get('.compose-tweet-btn').click();

        cy.get('textarea[name="tweet"]').type('That was an Attack on Titan easter egg πŸ₯š πŸ˜„');

        cy.wait(3000);

        cy.get('.post-tweet-btn').click();
    });

    it('user posts a third tweet', () => {
        // Post a tweet

        cy.get('.compose-tweet-btn').click();

        cy.get('textarea[name="tweet"]').type(
            'The Rumbling arrives on Marley 😱 https://www.youtube.com/watch?v=wT2H68kEmi8'
        );

        cy.wait(3000);

        cy.get('.post-tweet-btn').click();
    });
});
Enter fullscreen mode Exit fullscreen mode

Deployment

When you have completed building your application the final step is deployment. You need to get your application online so that everyone can see it. There are dozens of platforms out there but here are my top 5 platforms in no particular order.

  1. Netlify
  2. Vercel
  3. Heroku
  4. DigitalOcean
  5. AWS

Final Thoughts

We covered all of the MERN stack including TypeScript, SQL, Test Driven Development, End to End Testing and even Docker! Congratulations you just became super awesome because you boosted your skills and job prospects πŸ”₯πŸš€

Play around with the databases and React frontend there is so much you can do with it. Like for example creating more Storybook components, integration tests, adding the functionality to edit tweets and getting media like images and video to show up in the tweets.

Top comments (22)

Collapse
 
code_rabbi profile image
Emeka Orji

This is a whole lot! Well-done!!

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thanks glad you enjoyed it!

Collapse
 
nialljoemaher profile image
Niall Maher

Great work! This should be an e-book.❀️

Collapse
 
andrewbaisden profile image
Andrew Baisden

Yes I did consider putting this on Gumroad because I don't have any products on there yet. And some people have already asked if I have anything. It's my best article yet though!

Collapse
 
andresbecker profile image
Andres Becker

Thanks for writing this.

Collapse
 
aaravrrrrrr profile image
Aarav Reddy

Wonderful post

Collapse
 
dinerdas profile image
Diner Das

Wonderful post

Collapse
 
ambriel profile image
Ambriel Goshi

Thanks, great read.

Collapse
 
blackwatch1910 profile image
Devanshu Yadav

It's quite exciting, everything at one place, love it.

Collapse
 
cyebukayire profile image
Peace

This is so interesting. I'll take a look at it carefully during my holidays. Thank you Andrew, you're awesome:)

Collapse
 
imiahazel profile image
Imia Hazel

Thanks a lot.

Collapse
 
gene profile image
Gene

πŸš€πŸš€πŸš€πŸš€ The effort to put all this into one place πŸ‘πŸΌπŸ‘πŸΌπŸ‘πŸΌπŸ‘πŸΌ
Thank you for this.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.