Social Networking Application using React.js and Nest.js. Part-1

Fahad Ali
10 min readMar 29, 2023

--

This is my first article on [Medium](https://medium.com). I’ll explain each and every step, how social networking applications are created, how bi-directional communication works, and real-time communication between users.

The technologies that I’ll be using in this series will be:

  • Next.js (The React Framework for the Web)
  • Typescript (Static Typing for Javascript)
  • Nest.js (A progressive Node.js framework for building efficient,
    reliable, and scalable server-side applications.)
  • MongoDB (Document based Database)
  • Mantine (React component library)

***

Let’s get started.

Create a folder of your choice and name it whatever you want.

Let’s create a nextjs application. I’ll be using the latest version of Nextjs.

npx create-next-app@latest - typescript client

I ran the command and it will generate the folder named as **client** and all the files will be inside in the client folder.

  • package.json file

Running the Application

Open the terminal in the client directory
Run the following command to start the project on localhost:3000

Install ui library

npm install @mantine/core @mantine/hooks @mantine/nprogress @mantine/modals @mantine/spotlight @mantine/carousel embla-carousel-react @mantine/dropzone @mantine/tiptap @tabler/icons@1.119.0 @tiptap/react @tiptap/extension-link @tiptap/starter-kit @mantine/notifications @mantine/dates dayjs @mantine/form @mantine/next @emotion/server @emotion/react

Here are a few steps in order to use @mantine library.

Open the app.tsx file located inside the pages folder and edit the contents.

import type { AppProps } from 'next/app'

import { MantineProvider } from '@mantine/core';
export default function App({ Component, pageProps }: AppProps) {
return <MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
/** Put your mantine theme override here */
colorScheme: 'light',
}}
>
<Component {...pageProps} />
</MantineProvider>

}

I have also edited the _document.tsx file.

import { createGetInitialProps } from '@mantine/next';
import Document, { Head, Html, Main, NextScript } from 'next/document';

const getInitialProps = createGetInitialProps();

export default class _Document extends Document {
static getInitialProps = getInitialProps;

render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

Sign Up Page

In this article, I’ll be working on the Sign-up page.
I’ll keep it to a minimum and short.

Create signup.tsx

Copy and paste the code.

import { COLORS } from "@/utils/colors";
import { Button, Card, Center, Container, Input, Stack, Title, createStyles } from "@mantine/core";
import Head from "next/head";


const useStyles = createStyles((theme) => {
return {
containerBackgroundColor: {
backgroundColor: COLORS.BG
}
}
});

export default function SignupPage() {
const { classes } = useStyles();
return <Stack h={"100vh"} align="center" justify="center" className={classes.containerBackgroundColor}>
<Head>
<title>Sign Up</title>
</Head>
<Card shadow="sm" p="lg" w={800} radius="md" withBorder>
<Title align="center">Register an account</Title>

<Container mt={20}>
<form>
<Input placeholder="Email Address" />
<Input my={10} placeholder="Password" />
<Center>
<Button w={"100%"}>
Sign Up
</Button>
</Center>
</form>

</Container>
</Card>
</Stack>
}

Here, COLORS is an object from the colors.ts file. I have created a colors.ts file in the root of the folder.
The structure of the folder currently looks like this:

colors.ts file

The source code of the colors.ts file:

export const COLORS = {
BG: "#F2F2F2",
HEADING_TEXT: "#495057"
}

Nest.Js

I am going to create a Nestjs project where I will create APIs.

Open the command line and write the following commands.

Nest will automatically set up the project and install the required dependencies.

Install MongoDB into the Nestjs project.

npm install --save @nestjs/mongoose mongoose

npm install --save-dev @types/mongoose

First, we will connect to the mongo database using TypegooseModule.forRoot.

//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
imports: [
MongooseModule.forRoot("mongodb://localhost:27017/chat"),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }

Let’s generate a module for user registration by running the following command.

nest g module users
// users.module.ts

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';

@Module({
controllers: [UsersController]
})
export class UsersModule {}

Also, generate a controller for the user:

nest g controller users
// users.controller.ts

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

@Controller('users')
export class UsersController {}

Writing a Schema for the User to save documents into the database.

Create a user.schema.ts file in the users folder and either write or copy and paste the following code.

// user.schema.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";

export type UserDocument = HydratedDocument<User>;

@Schema({ timestamps: true })
export class User {
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true, })
password: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

With Mongoose, everything is derived from a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection. Schemas are used to define Models. Models are responsible for creating and reading documents from the underlying MongoDB database.

Schemas can be created with NestJS decorators, or with Mongoose itself manually. Using decorators to create schemas greatly reduces boilerplate and improves overall code readability.

The @Schema() decorator marks a class as a schema definition. It maps our User class to a MongoDB collection of the same name, but with an additional “s” at the end - so the final mongo collection name will be users. This decorator accepts a single optional argument which is a schema options object. Think of it as the object you would normally pass as a second argument of the mongoose.Schema class' constructor (e.g., new mongoose.Schema(_, options))).

The @Prop() decorator defines a property in the document. For example, in the schema definition above, we defined three properties: email, password.

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './user.schema';

@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])
],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule { }

The MongooseModule provides the forFeature() method to configure the module, including defining which models should be registered in the current scope. If you also want to use the models in another module, add MongooseModule to the exports section of UsersModule and import UsersModule in the other module.

The next step is to set up the validation of coming data from the front end.

For this, I’ll be using class-validator and class-transformer packages.

npm i --save class-validator class-transformer

The ValidationPipe is exported from the @nestjs/common package.

The ValidationPipe makes use of the powerful class-validator package and its declarative validation decorators. The ValidationPipe provides a convenient approach to enforce validation rules for all incoming client payloads, where the specific rules are declared with simple annotations in local class/DTO declarations in each module.

In the main.ts file, set up a global pipe to validate the data.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from "@nestjs/common"

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true
}))
await app.listen(3001);
}
bootstrap();

The next step is to create a auth.dto.ts (Data Transfer Object) class to transform the payload. I used auth because the sign-up and sign-in payloads will be the same.

import { IsEmail, IsNotEmpty, MinLength } from "class-validator";

export class AuthDto {
@IsEmail()
email: string;

@MinLength(5)
@IsNotEmpty()
password: string;
}
  • @IsEmail() Decorator checks if the string is an email. If the given value is not a string, then it returns false.
  • @IsNotEmpty() Decorator checks if given value is not empty (!== ‘’, !== null, !== undefined).
  • @MinLength() Decorator checks if the value is greater than or equal to the allowed minimum value.

UserController.ts class will be used to handle incoming requests and send the appropriate responses. One of the patterns that Nestjs uses is a service-based pattern. The users.server.ts class will be used for all the logic to create a user account and to sign-in the user.

nest g s users
import { BadRequestException, Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { User, UserDocument } from './user.schema';
import { InjectModel } from '@nestjs/mongoose';
import { AuthDto } from './auth.dto';
import { hash } from "bcryptjs";

@Injectable()
export class UsersService {

constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>
) {

}


async createUser(data: AuthDto) {
try {
const isUserFound = await this.getUserByEmail(data.email);
if (isUserFound) {
throw new BadRequestException("User already exists");
}

const password = await hash(data.password, 10);
const newUser = new this.userModel({ ...data, password });
await newUser.save();
return {
success: true,
message: "User created successfully"
}
} catch (error) {
const err = error as Error;
console.log(err.message);
throw new BadRequestException(err.message)
}
}

async getUserByEmail(email: string) {
return await this.userModel.findOne({ email }).exec()
}

}
  • @Injectable(): Decorator that marks a class as a provider. Providers can be injected into other classes via constructor parameter injection using Nest’s built-in Dependency Injection (DI) system.
  • Model: Models are responsible for creating and reading documents from the underlying MongoDB database.
  • @InjectModel(): Once you’ve registered the schema, you can inject a User model into the UsersService using the @InjectModel() decorator.
  • createUser(): It creates a new user in the database. The function first checks if there is an already existing user with the same email, then return an error otherwise hash the password using the hash() function from bcryptjs and saves the encrypted password.
  • getUserByEmail(): If the user is found by matching the email then it returns the user otherwise it returns null.

Install bcryptjs using :

npm i --save-dev @types/bcryptjs bryptjs

The next step is to update the users.controller.ts file.

import { Controller, Body, Post,} from '@nestjs/common';
import { AuthDto } from './auth.dto';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
constructor(
private usersService: UsersService
) { }

@Post("signup")
async signupUser(@Body() authDto: AuthDto) {
return await this.usersService.createUser(authDto)
}
}
  • constructor function: Nestjs will automatically inject usersService from the Di Container.
  • signUpUser: The method takes in authDTO payload as an argument and then calls the createUser method from the usersService.
  • @Post(): The decorator is used for post requests and optionally takes and URL to map to. In this case, the URL will be HTTP:://localhost:3031/users/signup.
  • Nestjs will automatically insert “/” at the start of the endpoint if there isn’t any found.

The last thing in this section is to remove the password in the JSON response.

In the user.schema.ts file:


@Schema({
timestamps: true, toJSON: {
transform(doc, ret, options) {
delete ret["password"]
},
}
})
export class User {
@Prop({ required: true, unique: true })
email: string;


@Prop({ required: true, })
password: string;
}
  • @Schema() Decorator optionally takes an options object for the user schema.
  • timestamps: The timestamps option tells mongoose to assign createdAt and updatedAt fields to your schema.
  • toJSON: Exactly the same as the toObject option but only applies when the document’s toJSON method is called. “(Please note that the toJSON method is called when the response.json() is called on a server, mongoose will call the toJSON method from the schema for the user schema.)”
  • This is useful the certain properties are to be removed from the object.
  • transform: if set, mongoose will call this function to allow you to transform the returned object. The transform function for the schema type is only called when calling toJSON() or toObject().
  • First Argument doc (the original mongoose document), 2nd Argumentret (the returned object), and last one theoptions (the schema options).

The next step is to integrate the sign-up API with the sign-up page component.

Connecting Sign in page with the client.

  • Create an API folder at the root of the folder.
  • Create an index.ts file.

Inside the index.ts file:

import axios, { AxiosInstance } from 'axios';
export const axiosInstance: AxiosInstance = axios.create({
baseURL: "http://localhost:3001"
});
  • axiosInstance is a variable that holds the base URL for the API endpoint.

Create a new user.api.ts file inside the frontend folder.

import { IUser } from "@/types/user.types";
import { axiosInstance } from ".";

export type IAuthResponse = {
success: boolean,
message: string,
user: IUser
}

export type IAuthData = {
email: string,
password: string,
}


export async function authApi<ReturnResponse extends IAuthResponse = IAuthResponse
, D extends IAuthData = IAuthData>(url: string, data: D)
: Promise<ReturnResponse> {

const response = await axiosInstance.post<ReturnResponse>(`/users${url}`, data);
return response.data;
}
  • IAuthResponse type is the response type from the server.
  • IAuthData the data will be sent from the client to the server.
  • auth function that will be used for both registrations of the user and sign-in of the user. It receives 2 arguments, the first URL and the second data. The data should have properties of “email” and “password”.

user.types.ts files contain types for the user.

// user.types.ts
export type IUser = {
email: string,
_id: string,
createdAt: string,
updatedAt: string
};

The next step is to integrate the sign-in API with the sign-in page. Head back to your sign-in page component.


export default function SignupPage() {
const { classes } = useStyles();
const [data, setData] = useState({ email: "", password: "" });
const [loading, setLoading] = useState(false);

function handleChange(e: ChangeEvent<HTMLInputElement>) {
setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
}

async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
try {
const response = await authApi("/signup", data);
notifications.show({
message: response.message,
title: "Success",
radius: "md",
color: "green",
autoClose: 3000
});
} catch (error) {
const err = error as AxiosError<IAuthErrorResponse>;
if (err.response && err.response.data) {
notifications.show({
message: err.response.data.message,
title: "Failed",
radius: "md",
color: "red",
autoClose: 3000
});
}
console.log(err);
} finally {
setLoading(false);
}
}

return <Stack h={"100vh"} align="center" justify="center" className={classes.containerBackgroundColor}>
<Head>
<title>Sign Up</title>
</Head>
<Card shadow="sm" p="lg" w={800} radius="md" withBorder>
<Title align="center">Register an account</Title>

<Container mt={20}>
<form onSubmit={handleSubmit}>
<Input onChange={handleChange}
type="email" name="email" placeholder="Email Address"
value={data.email}
/>
<Input name="password" my={10}
onChange={handleChange}
type="password"
value={data.password}
placeholder="Password" />
<Center>
<Button
disabled={data.email.length === 0 || data.password.length === 0}
loading={loading}
w={"100%"} type="submit">
Sign Up
</Button>
</Center>
</form>

</Container>
</Card>
</Stack>
}
  • const [data, setData] = useState({ email: “”, password: “” }): This is
    the initial state of the form. The loading will start when the request will be sent to the server.
  • const [loading, setLoading] = useState(false): The loading will be started when the form is submitted.
  • handleChange(): The function will be called whenever the input field is changed.

handleSubmit():

  • The function first calls e.preventDefault(), the preventDefault() method cancels the event, and the default action that belongs to the event will not occur.
  • Then it sets the loading state to true.
  • Inside the try-catch block, the data will be sent to the server.
  • If the user isn’t found or something else goes wrong, an error notification will be generated.

--

--