Next.js, NestJS, MobX and how I structured my open-source project
Tags: architecture, webdev, node
Published: 2021-09-17 22:29:02

This is part of a series of articles about my open-source project ([backend](https://github.com/zvikfir/MyWay-server) and [frontend](https://github.com/zvikfir/MyWay-client)). if you want to join me feel free to contact me at - <zvikfir10@gmail.com>.

## Introduction

Recently, I have re-started working on an open source project I came up with last year, called _MyWay_ (it is named after my and my dog trainer girlfriend's dog "Maui"). This project aims to serve as a Customer Management System (CMS) platform for dog trainers, helping them to keep track of their work with each customer, including abilities such as sending a summary of the last session directly to the customer, or plan the progress that needs to be made along the training sessions in a chart that is easy to use and read.

In the beginning, I started the project with the technologies I knew back then, which were _React_, _Express_ over _Node_, and _MongoDB_ (it is worth mentioning that I didn't have a lot of experience with full-stack, and most of what I knew were from the [_freeCodeCamp_](freecodecamp.org) courses I took on my free time). These worked great for me last year as I worked on the project for a few weeks. However, this time, when I tried to keep using them I felt a lot of discomfort. I felt that for a large portion of my code - I don't really know if it works, or how well it works.

I tried to take inspiration from several template projects I found on _Github_, but it was hard for me since each such template took its own approach into doing things. I personally needed to have a framework that would dictate a project structure and layout.

I consulted an open source community Facebook group named [_"Pull Request"_](https://www.facebook.com/groups/PullRequest), and I was recommended to use [_NestJS_](https://nestjs.com). At first, it was really hard for me to get used to a whole new framework, which is written in _Typescript_ rather than _Javascript_ (although it is possible to use it with JS), and contains a whole new set of classes, objects, and methodologies. Nonetheless, it felt like the right thing to do, as Nest helps to keep your code organized, and much less error-prone.

I tried to find a similar solution to the front-end part, and eventually I chose [_Next.JS_](https://nextjs.org). It mainly provides the ability to [_pre-render_](https://nextjs.org/learn/basics/data-fetching/pre-rendering), but I liked that it contained a special `pages` directory which automatically created routes according to the file's name. However, NextJS does not provide a clear project structure and it still gives much freedom to each developer, which didn't fit for me.

I wound up combining several ideas (the main ones were taken from [this post](https://dev.to/cakasuma/using-mobx-hooks-with-multiple-stores-in-react-3dk4) covering the use of [_MobX_](https://mobx.js.org/README.html) with React hooks, and from [this GitHub project](https://github.com/arielweinberger/task-management-frontend) taken from Ariel Weinberger's excellent Udemy course covering NestJS) into what I think is a good project structure, and this is what I'm going to cover in this post.

## The Proposed Structure

The structure I will cover here uses MobX, a very popular state management library. Although MobX is not an essential part and you can achieve a similar solution without using it, I think it is of great value and that's why I included it.
MobX is used to provide all the components in our app with an instance of the `RootStore`. The `RootStore` is a class that creates all the services your app requires, as well as all the stores. While creating each store, the `RootStore` makes sure to provide an instance of itself, so that each store will be able to access other stores, and an instance of the its dependent services.

Before I explain each step in detail, you can view almost all the relevant changes I have made in the MyWay project in [this commit](https://github.com/zvikfir/MyWay-client/commit/b526b6dcd0ae1d26224f620f8540ca2f8d06a6fc).

First of all, create three folders: `services`, `stores`, and `dto`. In the `stores` folder, create a file named `stores/index.ts` and in it create the `RootStore` class:

`stores/index.ts`
```typescript
export class RootStore {
  constructor() {
    const authService = new AuthService();

    this.userStore = new UserStore(this, authService);
  }
}

export const StoresContext = createContext(new RootStore());
export const useStores = () => useContext(StoresContext);
```

This code presents how you can create services in the `RootStore` constructor so that each of this services will be a Singleton, since they are created only once, and create all the needed stores in your application. In this example we have the `AuthService` and the `UserStore`. The `AuthService` will contain logic related to authentication, such as login, register, and logout. The `UserStore` is a store that contains information about the user in the application, and it might want to save the user information once it is logging into its account, so that all the components in the system can use it.

As mentioned, note that each store is given the `RootStore` instance (`this`), and the services it requires.

Another important part of that code is how we expose it to all of the components in our app. To do so, we make use of React's context. We first use `createContext` to create a context containing the `RootStore` instance, and then we export a `useStores` function that will easily allow us to use the context created.

Next up, let's create the `AuthService` class.

We will most likely have many services in our app. To simplify their creation, we'll create a base class from which they will inherit. This base class will abstract the use of http libraries such as the built-in `fetch` or `axios`. This way, should the need to switch to a more modern library arises, you can do so easily (you can read a more detailed explanation [here](https://kyleshevlin.com/facade-pattern?fbclid=IwAR1wcVlY5SAWQjf316lJRxr672odRW6zFmvFufd564foM3wwp1gcvSeKGN4)).

Create a file named `services/base-http.service.ts`:

`services/base-http.service.ts`
```typescript
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import Router from "next/router";
import { APIErrorResponse } from "../dto/api/api-error-response";
import { APIResponse } from "../dto/api/api-response";

// Taken from https://github.com/arielweinberger/task-management-frontend/blob/master/src/services/base-http.service.js

export default class BaseHttpService {
  BASE_URL = process.env.BASE_URL || "http://localhost:3000";
  //   _accessToken: string = null;

  async get<T = any>(
    endpoint: string,
    options: AxiosRequestConfig = {}
  ): Promise<T | void> {
    Object.assign(options, this._getCommonOptions());
    return axios
      .get<APIResponse<T>>(`${this.BASE_URL}${endpoint}`, options)
      .then((res: AxiosResponse<APIResponse<T>>) => res.data.data)
      .catch((error: AxiosError<APIErrorResponse>) =>
        this._handleHttpError(error)
      );
  }

  async post<T = any>(
    endpoint: string,
    data: any = {},
    options: AxiosRequestConfig = {}
  ): Promise<T | void> {
    Object.assign(options, this._getCommonOptions());
    return axios
      .post<APIResponse<T>>(`${this.BASE_URL}${endpoint}`, data, options)
      .then((res: AxiosResponse<APIResponse<T>>) => res.data.data)
      .catch((error: AxiosError<APIErrorResponse>) =>
        this._handleHttpError(error)
      );
  }

  async delete<T = any>(
    endpoint: string,
    options: AxiosRequestConfig = {}
  ): Promise<T | void> {
    Object.assign(options, this._getCommonOptions());
    return axios
      .delete<APIResponse<T>>(`${this.BASE_URL}${endpoint}`, options)
      .then((res: AxiosResponse<APIResponse<T>>) => res.data.data)
      .catch((error: AxiosError<APIErrorResponse>) =>
        this._handleHttpError(error)
      );
  }

  async patch<T = any>(
    endpoint: string,
    data: any = {},
    options: AxiosRequestConfig = {}
  ): Promise<T | void> {
    Object.assign(options, this._getCommonOptions());
    return axios
      .patch<APIResponse<T>>(`${this.BASE_URL}${endpoint}`, data, options)
      .then((res: AxiosResponse<APIResponse<T>>) => res.data.data)
      .catch((error: AxiosError<APIErrorResponse>) =>
        this._handleHttpError(error)
      );
  }

  _handleHttpError(error: AxiosError<APIErrorResponse>) {
    if (error?.response?.data) {
      const { statusCode } = error?.response?.data;

      const requestUrl = error.response?.config.url;

      if (
        statusCode !== 401 ||
        requestUrl?.endsWith("/api/auth/login") ||
        requestUrl?.endsWith("/api/auth/register")
      ) {
        throw error.response.data;
      } else {
        return this._handle401(error);
      }
    } else {
      throw error;
    }
  }

  _handle401(error: AxiosError<APIErrorResponse>) {
    this.get("/api/auth/refresh")
      .then(() => axios.request(error.config))
      .catch((e) => Router.push("/login"));
  }

  _getCommonOptions() {
    // const token = this.loadToken();

    // return {
    //   headers: {
    //     Authorization: `Bearer ${token}`,
    //   },
    // };
    return {};
  }

  //   get accessToken() {
  //     return this._accessToken ? this._accessToken : this.loadToken();
  //   }

  //   saveToken(accessToken : string) {
  //     this._accessToken = accessToken;
  //     return localStorage.setItem("accessToken", accessToken);
  //   }

  //   loadToken() {
  //     const token : string = localStorage.getItem("accessToken") as string;
  //     this._accessToken = token;
  //     return token;
  //   }

  //   removeToken() {
  //     localStorage.removeItem("accessToken");
  //   }
}
```

In this class, we expose the basic functions that are used in any http library: `get`, `post`, `put`, `patch`, and `delete`. In each function, we simply call the http library we would like to use. In this case, it is `axios`. You can easily use any other library you would like.

Since we are using NestJS, our API usually has a uniform response structure. We make sure to import and use the relevant interfaces so programmers who read our code can understand it more easily:

`dto/api/api-response.ts`
```typescript
export interface APIResponse<T> {
  data: T;
}
```

`dto/api/api-error-response.ts`
```typescript
export interface APIErrorResponse {
  statusCode: number;
  message: string;
  error?: string;
}
```

Another benefit we have from using this base class for our services is the ability to catch errors on any request sent in our application, and to apply a certain logic to it. For example, in case of authentication we might want to intercept any error with a status code of 401. In MyWay, I implemented the authentication with a JWT access token and a refresh token that are saved as cookies, so if I get a 401 response, I want to try to use my refresh token to get a new access token. You can see the logic applied in the `_handle401` function. In addition, you can see in the commented code how to implement a strategy which saves the tokens in local storage.

Once we have this base class set up, we can now create the authentication service class:

`services/auth.service.ts`
```typescript
import { LoginDto } from "../dto/auth/login.dto";
import { RegisterDto } from "../dto/auth/register.dto";
import { SessionUserDto } from "../dto/auth/session-user.dto";
import BaseHttpService from "./base-http.service";

export default class AuthService extends BaseHttpService {
  async login(loginDto: LoginDto): Promise<SessionUserDto> {
    return (await this.post<SessionUserDto>(
      "/api/auth/login",
      loginDto
    )) as SessionUserDto;
  }

  async register(registerDto: RegisterDto): Promise<void> {
    return await this.post("/api/auth/register", registerDto);
  }
}
```

This code is quite self explanatory, so we'll move right to creating our `UserStore` class.

`stores/user.store.ts`
```typescript
import { makeAutoObservable } from "mobx";
import { RootStore } from ".";
import { LoginDto } from "../dto/auth/login.dto";
import { RegisterDto } from "../dto/auth/register.dto";
import { SessionUserDto } from "../dto/auth/session-user.dto";
import AuthService from "../services/auth.service";

export default class UserStore {
  user: SessionUserDto | null;

  constructor(
    private readonly rootStore: RootStore,
    private readonly authService: AuthService
  ) {
    this.user = null;
    makeAutoObservable(this);
  }

  async login(loginDto: LoginDto): Promise<void> {
    this.user = await this.authService.login(loginDto);
  }

  async register(registerDto: RegisterDto): Promise<void> {
    await this.authService.register(registerDto);
    const { email, password } = registerDto;
    const loginDto: LoginDto = { email, password };
    this.user = await this.authService.login(loginDto);
  }
}
```

In each store, we can create the state that we would like to expose to components which use it. In this case, the state contains the user which is currently logged in. You can also see how decoupling the logic from the components helps us to avoid code duplication: in the `register` function, instead of re-writing the same logic of sending an API call to the server, and handling the possible errors all over again, we simply use the `login` function which is already in the same store. In addition, in case we wanted to use some logic from another store, we would simply do so like this:

```typescript
this.rootStore.someOtherStore.someFunction();
```

Remember that through the `rootStore` field we can access all the other stores in our application.

Now that this is covered, let's see how we use the stores and services we created in our components. Let's take the `LoginForm` component as an example:

`components/auth/login.form.component.tsx`
```typescript
...
 import { useStores } from "../../stores";
 import { APIErrorResponse } from "../../dto/api/api-error-response";
import { observer } from "mobx-react-lite";

 const LoginForm = observer(function LoginForm() {
   const { userStore } = useStores();

   return (
     <Formik
       initialValues={{
         email: "",
         password: "",
       }}
       onSubmit={(values, { setSubmitting, setStatus }) => {
         userStore
           .login(values)
           .then(() => {
             setStatus({ message: "You have logged in successfully." });
             setTimeout(() => Router.push("/"), 2000);
           })
           .catch((e: APIErrorResponse) => {
             setStatus({ error: e.message });
           })
           .finally(() => setSubmitting(false));
       }}
...

export default LoginForm;
```

The only change we have made is to declare a `userStore` variable taken from the `useStores()` function call. With this store, we can use all the functions it exposes, as well as read its state like this:

```typescript
const { user } = userStore;
```

That's it!

We have managed to create a service oriented project structure that can help us maintain organization in our project, separating logic and state from our components, so they can take care of only the view layer, avoid code duplication, and help us avoid runtime errors.

This project is still a work in progress, so you can see in later commits examples of more services and stores.

I hope that this post has been somewhat helpful to you, and feel free to leave some feedback. I'd love to hear from you.

I'd like to thank Shmuel Shoshtari for both motivating me to write this post, and for valuable feedback regarding this post and the project.