REST API USING TYPESCRIPT ( CRUD )
Tags: webdev, restapi, typescript, node
Published: 2023-10-19 08:20:08

An MVC Express Typescript App with CRUD operations, including File Upload and proper Authentication and Authorization using JWT 

---


## In this Article, we'll delve into the process of creating a RESTful API that implements CRUD operations using JWT for authorization

>A basic CRUD api has options to CREATE / READ / UPDATE and DELETE items , along with Authentication and Authorisation in place , so as to ensure that the app transactions are secure

> 👀 NOTE : This APP has not been created using the best testing practices ( i.e TDD or BDD ) , but we'll mitigate this issue   later  by creating a similar app , where WE'LL BE FOLLOWING TDD / BDD practices  , and buiding it using the principles of  <u>[CEAN ARCHITECTURE](https://www.chiranjeevthomas.com/article/clean-architecture)</u>

---

## An Overview Of The Project :
<br/>

>14.  ✅ The project  is an e-commerce app that lets the ADMIN add products to the backend (The product has some text details and a picture )
>15.  ✅ Sign up: Users can register by creating a new account using an `email address` and `password`.
>15.  ✅ Authentication: Registered users can `login` and `logout`.
>16.  ✅ Only `admins` are allowed to create products
>17.   ✅ Protected user profile: Only registered users can view individual user details after signing in.
>18.  ✅ We'll be using JWT to provide Authorization  / Authentication in the app
---

## The API  we'll be creating :
<br/>
<img  alt="api_structure" src="https://res.cloudinary.com/dde6rnocj/image/upload/v1691905643/api_routes_4cfd29f90e.png" width="80%" height="40%"/>


<br/>

-----

## **Let's first talk about the USER MODEL**

* A person who registers on the app can either have the  <u>role </u> of a <u>`user`</u> or an <u>`admin`</u> . 
* An admin has all permissions but a user is limited to only <u>`viewing  products`</u> and their <u>`own information`</u>



```

import { Document, Schema, model } from 'mongoose';

// Define the interface for the User document
const enum Role {
  ADMIN = 'admin',
  USER = 'user',
}
export interface IUser extends Document {
  username: string;
  email: string;
  password: string;
  role: Role;
}

// Declare the Schema of the Mongo model
const userSchema = new Schema<IUser>(
  {
    username: {
      type: String,
      required: true,
      index: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
    role: {
      type: String,
      enum: [Role.ADMIN, Role.USER],
      default: Role.USER,
    },
  },
  {
    timestamps: true,
  }
);

// Export the model
export default model<IUser>('User', userSchema);

```


>So in the above <u>`userSchema`</u> , we define the<u> `username`</u> , <u>_`email`_</u> and <u>`password`</u> for a user to register , along with a <u>`role`</u> that's set to <u> `user` </u> by default 

>The roles are set as `Enums` deliberately , so as to avoid illegal assignment .
```javascript
const enum Role {
  ADMIN = 'admin',
  USER = 'user',
}
```

---

## Now , let's  look at the `registerUser` controller

### Steps taken to register  a user  : 

> Step 1 :  [+] validate the request and extract info
> 
>  Step 2 : [+] check if user is in database already
>  
>  Step 3: [+] hash password to encrypt
>  
>  Step 4: [+] create user object
>  
>  Setp 5: [+] save user to database
>  
>  Setp 6 : [+] create access_token ( JWT )
>  
>  Step 7 : [+] create refresh_token ( JWT )
>  
>  Step 8 : [+] save refresh_token to db
>  
>  Step 9 : [+] send json response to frontend ( with  the newly created refresh_token and the access_token )
>  
>  Step 10 :[+] send async exception to `next(err)`


```
//registerUser.ts

import { NextFunction, Request, Response } from 'express';
import { NextFunction, Request, Response } from 'express';
import { REFRESH_TOKEN_SECRET } from '../../config';
import { RefreshToken, User } from '../../models';
import { IUser } from '../../models/UserModel';
import CustomErrorHandler from '../../services/CustomErrorHandler';
import EncryptionService from '../../services/EncryptionService';
import JwtService from '../../services/JwtService';
import { RegisterUserRequest, RegisterUserResponse, Role } from '../../types';
import { registerSchema } from '../../validation';

type Tokens = {
  access_token: string;
  refresh_token: string;
};


const registerUser = async (
  req: Request<RegisterUserRequest>,
  res: Response<RegisterUserResponse, Tokens>,
  next: NextFunction
): Promise<void> => {
  try {
    // [+] validate the request and extract info
    const { username, email, password } = await registerSchema.parseAsync(
      req.body
    );

    //[+] check if user is in database already
    const userExists = await User.exists({ email });

    if (userExists)
      return next(
        CustomErrorHandler.alreadyExists('This email is already taken')
      );

    //[+] hash password to encrypt
    const hashedPassword = await EncryptionService.getHashedToken(password);

    //[+] create user object
    const userInfo = {
      username,
      email,
      password: hashedPassword,
      role: Role.ADMIN,
    };

    //[+] save user to database
    const user: IUser = await User.create(userInfo);

    //[+] create access_token
    const access_token = await JwtService.sign({
      _id: user._id as string,
      role: user.role,
    });

    //[+] create refresh_token
    const refresh_token = await JwtService.sign(
      {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        _id: user.id,
        role: user.role,
      },
      '1y',
      REFRESH_TOKEN_SECRET
    );

    //[+] save refresh_token to db
    await RefreshToken.create({ token: refresh_token });

    // [+] send response
    const tokens: Tokens = { access_token, refresh_token };
    res.json(tokens);
  } catch (err) {
    next(err);
  }
};

export default {
  registerUser,
};

```

----

## Let's turn our attention to the multiple imports we have at the top of the `resgisterUser` code ( p.s : this is a bad sign , as the number of imports shows how coupled our code is with other modules )

```typescript

import { NextFunction, Request, Response } from 'express';
import { REFRESH_TOKEN_SECRET } from '../../config';
import { RefreshToken, User } from '../../models';
import { IUser } from '../../models/UserModel';
import CustomErrorHandler from '../../services/CustomErrorHandler';
import EncryptionService from '../../services/EncryptionService';
import JwtService from '../../services/JwtService';
import { RegisterUserRequest, RegisterUserResponse, Role } from '../../types';
import { registerSchema } from '../../validation';

```

---

## Validation using Zod : 

```typescript

// validationSchemas.ts
import { z } from 'zod';

/* The `.refine()` method is used to add additional validation rules to the validation schema. In
  this case, it is checking if the `password` field is equal to the `repeat_password` field. If
  they are not equal, it will throw an error with the specified message ("Passwords don't match")
  and the path of the error will be set to `['repeat_password']`. This allows for more specific
  error messages and error paths when validating the data. */
export const registerSchema = z
  .object({
    username: z.string().min(3).max(20).trim(),
    email: z.string().email().trim(),
    password: z.string().min(6),
    repeat_password: z.string().min(6),
  })
  .refine((data) => data.password === data.repeat_password, {
    message: " : Passwords don't match",
    path: ['repeat_password'], // path of error
  });

```

---


## Custom Error Handler :

>*A Convenient and Elegant way to handle errors is to create a <u>custom error handler </u> and define all errors over there
>
>*We will throw our custom errors if and when the need arises 
>
>*By following this pattern, we are in complete control of the declariation and summoning of Errors from a single source of truth


```typescript
class CustomErrorHandler extends Error {
  constructor(public status: number, public message: string) {
    super();
    this.status = status;
    this.message = message;
  }

  /**
   * The function returns a new instance of a custom error handler with a status code of 409 and a
   * given error message.
   * @param {string} message - The `message` parameter is a string that represents the error message to
   * be displayed.
   * @returns A new instance of the CustomErrorHandler class with a status code of 409 and the provided
   * message.
   */
  static alreadyExists(message: string) {
    return new CustomErrorHandler(409, message);
  }

  /**
   * The function returns a custom error handler with a 401 status code and a message indicating that
   * the user authentication failed due to an invalid password or email.
   * @param [message=password or email invalid] - The message parameter is a string that represents the
   * error message to be displayed when the user authentication fails. By default, the message is set
   * to 'password or email invalid'.
   * @returns A new instance of the CustomErrorHandler class with a status code of 401 and a message of
   * "password or email invalid".
   */
  static userAuthFailed(message = 'missing or invalid jwt') {
    /* The line `return new CustomErrorHandler(401, message);` is creating a new instance of the
  `CustomErrorHandler` class with a status code of 401 and the provided error message. This instance
  is then returned by the `wrongUserCredentials` static method. */
    return new CustomErrorHandler(401, message);
  }

  /**
   * The function returns a custom error handler with a 401 status code and a message indicating
   * invalid password or email.
   * @param [message=password or email invalid] - The message parameter is a string that represents the
   * error message to be displayed when the user credentials are invalid. The default value is
   * 'password or email invalid'.
   * @returns a new instance of the CustomErrorHandler class with a status code of 401 and a message of
   * "password or email invalid".
   */
  static wrongCredentials(message = 'password or email invalid') {
    return new CustomErrorHandler(401, message);
  }

  static notFound(message = 'Not Found') {
    return new CustomErrorHandler(404, message);
  }

  static unAuthorized(message = 'auth failed') {
    return new CustomErrorHandler(401, message);
  }

  static multerError(message = 'Multer File Upload Error') {
    return new CustomErrorHandler(401, message);
  }
}

export default CustomErrorHandler;
```

---
### **You can View the Complete Project on my GitHub profile** : <u> [here](https://github.com/code0monkey1/rest-api-structure)</u>