DEV Community

Cover image for REST API USING TYPESCRIPT ( CRUD )
Chiranjeev Thomas
Chiranjeev Thomas

Posted on

REST API USING TYPESCRIPT ( CRUD )

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 CEAN ARCHITECTURE


An Overview Of The Project :

  1. āœ… 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 )
  2. āœ… Sign up: Users can register by creating a new account using an email address and password.
  3. āœ… Authentication: Registered users can login and logout.
  4. āœ… Only admins are allowed to create products
  5. āœ… 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 :



api_structure


Let's first talk about the USER MODEL

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

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);

Enter fullscreen mode Exit fullscreen mode

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

The roles are set as Enums deliberately , so as to avoid illegal assignment .

const enum Role {
  ADMIN = 'admin',
  USER = 'user',
}
Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode

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 )


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';

Enter fullscreen mode Exit fullscreen mode

Validation using Zod :


// 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
  });

Enter fullscreen mode Exit fullscreen mode

Custom Error Handler :

*A Convenient and Elegant way to handle errors is to create a custom error handler 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

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;
Enter fullscreen mode Exit fullscreen mode

You can View the Complete Project on my GitHub profile : here

Top comments (0)