Creating a Project with Nest.js + Next.js
Tags: nestjs, nextjs, webdev, tutorial
Published: 2022-01-05 13:38:22

This is the first part in a series of articles about combining nest.js and NEXT.js. Today you will learn how to setup a project and choose a valid SSR strategy. [In the second part](https://dev.to/yakovlev_alexey/advanced-practices-for-nestjs-nextjs-projects-36g9) you will learn about Hot Module Replacement, more SSR techniques and subdirectory deployments.

> Hey, it's October 2022 and [Next.js 13](https://nextjs.org/blog/next-13) was recently released! It brings **lots** of changes and the new `app` directory. While I am working on discovering all the new features and use cases to update this tutorial (and perhaps `nest-next`), make sure to leave your feedback on `nest-next` with Next.js 13 in [this GitHub issue](https://github.com/kyle-mccarthy/nest-next/issues/92).

_This post is a translation of [the original articles by me on Habr](https://habr.com/ru/post/570886/). The translation had no input from experienced tech writers or editors. So any feedback towards correcting any mistakes is greatly appreciated._

## Table of Contents

-   [Table of Contents](#table-of-contents)
-   [Introduction](#introduction)
-   [Before we start](#before-we-start)
-   [Creating a nest.js application](#creating-a-nestjs-application)
-   [NEXT.js installation](#nextjs-installation)
-   [SSR data preparation](#ssr-data-preparation)
    -   [Using nest.js controller](#using-nestjs-controller)
    -   [Using direct services access](#using-direct-services-access)
    -   [By sending requests to itself](#by-sending-requests-to-itself)
         -   [Wrapper for fetch](#wrapper-for-fetch)
         -   [Passing path parameters](#passing-path-parameters)
-   [Next steps](#next-steps)

## Introduction

When choosing a framework for web development in 2022 you might be considering [NEXT.js](https://nextjs.org/). It is a React based framework that supports SSR/SSG, Typescript, great codesplitting and opinionated routing out of the box. It's truly a great choice for solo developers and teams alike. 

NEXT.js also offers some backend functionality with serverless functions. However, serverless may not be the way for you and you would like to use something you are more used to. [nest.js](https://nestjs.com/) is a great choice here - it follows MVC architecture familiar to many which makes it very scalable and extendable. 

> NEXT.js and nest.js are very similar in naming so I will be always typing NEXT.js (the React framework) in uppercase and  nest.js (the MVC backend framework) in lowercase to differenciate between them more easily.

But how do you integrate these two? How do you make them work together? The simplest option would be to create a sophisticated (or not so sophisticated) proxy that would forward API requests to nest.js and all others to NEXT.js. But this might not be an option for you if you are limited by the amount of microservices you can use or you might not want a real monorepo in order to share code between services.

Luckily there is an easy option for you to embed Next.js right in your nest.js server. Meet [`nest-next`](https://www.npmjs.com/package/nest-next) - a view renderer for nest.js which uses NEXT.js to render pages right from a controller with a single decorator. However this leads to a few caveats in the inner workings of your application. Let's create a simple `nest-next` application together from the ground up so that we can discover these caveats and I can tell you about some of the best practices I and my colleagues discovered using this technology.

> Throughout the article I will try to delve as little as possible into each of the frameworks and will primarily focus on the bridge between the two. When certain knowledge about a framework is needed I will leave links to official documentation.

## Before we start

It's highly likely that you or your team opted for NEXT.js before choosing a backend. Here I recommend you to stop temporarily and seriously consider [NEXT.js server features](https://nextjs.org/docs/api-routes/introduction) - you might not need a real backend for many use cases like a simple backend-of-the-frontend render server. Using two frameworks and maintaining a bridge between them would be an overhead.

> You should only consider using `nest-next` if you are either planning a real node.js backend or you already have certain Express/fastify/nest.js infrastructure you plan to employ.

The article is fairly large since it covers most of the quirky details about the frameworks and it has been split into two parts. In the first one we will create a simple application from the ground up and show solutions the very basic problems you might have with SSR. If you consider yourself an experienced developer in this stack it might be more interesting for you to start [on the second part](https://dev.to/yakovlev_alexey/advanced-practices-for-nestjs-nextjs-projects-36g9) where I talk about some of the more advanced use cases like Hot Module Replacement, SSR techniques and deployment in a subdirectory.

And finally: for those who prefer skipping the article and getting right into code - you can find the source code for this article on my GitHub - [https://github.com/yakovlev-alexey/nest-next-example](https://github.com/yakovlev-alexey/nest-next-example) - commit history mostly follows the article.

## Creating a nest.js application

First we would need a nest.js application as our base. We will use nest.js CLI to generate a template.

```
npx @nestjs/cli new nest-next-example
```

Follow the instructions. I had chosen yarn as my package manager and will leave command snippets for yarn in the article but I assume you are familiar with package managers will be fine using npm in this article.

Upon command completion we will get a mostly empty project which may start instantly. I will remove all the test files (`test` directory and `app.controller.spec.ts`) from the project since we are not going to create any tests in this article.

I also recommend using the following directory structure that resembles a monorepo

```bash
└── src
    ├── client # client code: hooks, components, etc
    ├── pages # actual NEXT.js pages
    ├── server # nest.js server code
    └── shared # common types, utils, constants etc
```

Let's make the necessary changes to the nest.js configuration to support our new layout.

```json
// ./nest-cli.json
{
    "collection": "@nestjs/schematics",
    "sourceRoot": "src",
    "entryFile": "server/main"
}
```

Now if we start the application using `yarn start:dev` we should see `"Hello world"` when visiting `localhost:3000` in the browser.

> Due to certain caveats in nest.js building pipelines you might see an error in your terminal. The error might looks something like this: `'Error: Cannot find module '.../dist/server/main'`. In that case you may temporarily set `"entryFile"` to just `"main"` - that should solve the issue.

## NEXT.js installation

 Now let's add NEXT.js to our project.
 
```bash
# NEXT.js and its peers
yarn add next react react-dom
# required types and eslint preset
yarn add -D @types/react @types/react-dom eslint-config-next
```
 
Next you should start NEXT.js development server using `yarn next dev`. Necessary changes to your tsconfig will be made as well as a (few) new files added including `next-env.d.ts`. NEXT.js will boot successfully. However if we now start nest.js server, we will discover that NEXT.js broke our typescript config. Let's create a separate config for nest.js - I will be reusing existing `tsconfig.build.json` as `tsconfig.server.json` with the following contents.
 
```json
// ./tsconfig.server.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "noEmit": false
    },
    "include": [
        "./src/server/**/*.ts",
        "./src/shared/**/*.ts",
        "./@types/**/*.d.ts"
    ]
}
```
 
Now nest.js works again. Let's update scripts section in our `package.json` file.
 
```json
// ./package.json
"scripts": {
    "prebuild": "rimraf dist",
    "build": "yarn build:next && yarn build:nest",
    "build:next": "next build",
    "build:nest": "nest build --path ./tsconfig.server.json",
    "start": "node ./dist/server/main.js",
    "start:next": "next dev",
    "start:dev": "nest start --path ./tsconfig.server.json --watch",
    "start:debug": "nest start --path ./tsconfig.server.json --debug --watch",
    "start:prod": "node dist/main",
    // ... lint/format/test etc
},
```
 
Let's add an `index` page and an `App` component to `src/pages` directory.

```typescript
// ./src/pages/app.tsx
import { FC } from 'react';
import { AppProps } from 'next/app';

const App: FC<AppProps> = ({ Component, pageProps }) => {
    return <Component {...pageProps} />;
};

export default App;
```

```typescript
// ./src/pages/index.tsx
import { FC } from 'react';

const Home: FC = () => {
    return <h1>Home</h1>;
};

export default Home;
```

Now when you start the app using `yarn start:next` you should the this page on `localhost:3000`.

> You should also add `.next` folder to your `.gitignore` - that is where NEXT.js stores its builds.

## Making acquaintance between the frameworks

Now we have two separate servers. But what we want is a single nest.js server making use of `nest-next`: so let's install it.

```bash
yarn add nest-next
```

Next we should initialize the newly installed `RenderModule` in `app.module.ts`.

```typescript
// ./src/server/app.module.ts
import { Module } from '@nestjs/common';
import { RenderModule } from 'nest-next';
import Next from 'next';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
    /* should pass a NEXT.js server instance
        as the argument to `forRootAsync` */
    imports: [RenderModule.forRootAsync(Next({}))],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}
```

Now we can make use of `@Render` decorator exported from nest. So let's create our first page controller in `app.controller.ts`.

```typescript
// ./src/server/app.controller.ts
import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}
    
    @Get()
    @Render('index')
    home() {
        return {};
    }
}
```

However when we start the app using `yarn start:dev` and open the desired page in web browser we will see an error in NEXT.js - the build was not found. Turns out the render server was booted in production mode and expected to see a ready made build of the frontend application. To fix this we should provide `dev: true` argument when initialising server instance.

```
// ./src/server/app.module.ts
imports: [
    RenderModule.forRootAsync(Next({ dev: true }))
],
```

Let's try again. Open `localhost:3000` in your browser and you will see a 404. Which is unexpected since we have both a controller and a NEXT.js page. Turns out `nest-next` is looking in a wrong folder. By default it uses `views` subdirectory in `pages` folder rather than the folder itself. I personally do not like this inconsistency with NEXT.js so let's specify `viewsDir: null` in our `RenderModule` instance.

```
// ./src/server/app.module.ts
imports: [
    RenderModule.forRootAsync(
        Next({ dev: true }),
        /* null means that nest-next 
            should look for pages in root dir */
        { viewsDir: null }
    )
],
```

> Adjacent to `viewsDir` is another `dev` option - this time for `nest-next` which enables more specific error serialization. I did not find this option useful but it is there for you if you need it.

Finally, when we open `localhost:3000` in the browser we see the page we described earlier in `index.tsx`.

## SSR data preparation

One of the primary NEXT.js advantages is the ability to easily fetch data required to statically or dynamically render a page. Users have a few options to do so. We will use `getServerSideProps` (GSSP) - the most up-to-date way of fetching dynamic data in NEXT.js. However `nest-next` properly supports other methods as well.

Time to add another page. Let's imagine that our `index` is a blog page. And the page we would be creating is a blog post by id.

Add the necessary types and controllers:

```typescript
// ./src/shared/types/blog-post.ts
export type BlogPost = {
    title: string;
    id: number;
};

// ./src/server/app.controller.ts
import { Controller, Get, Param, Render } from '@nestjs/common';

// ...

@Get(':id')
@Render('[id]')
public blogPost(@Param('id') id: string) {
 return {};
}
```

Add the new page:

```typescript
// ./src/pages/[id].tsx
import { GetServerSideProps } from 'next';
import Link from 'next/link';
import { FC } from 'react';
import { BlogPost } from 'src/shared/types/blog-post';

type TBlogProps = {
    post: BlogPost;
};

const Blog: FC<TBlogProps> = ({ post = {} }) => {
    return (
        <div>
            <Link href={'/'}>Home</Link>
            <h1>Blog {post.title}</h1>
        </div>
    );
};

export const getServerSideProps: GetServerSideProps<TBlogProps> = async (
    ctx,
) => {
    return { props: {} };
};

export default Blog;
```

And refresh our Home page:

```typescript
// ./src/pages/index.tsx
import { GetServerSideProps } from 'next';
import Link from 'next/link';
import { FC } from 'react';
import { BlogPost } from 'src/shared/types/blog-post';

type THomeProps = {
    blogPosts: BlogPost[];
};

const Home: FC<THomeProps> = ({ blogPosts = [] }) => {
    return (
        <div>
            <h1>Home</h1>
            {blogPosts.map(({ title, id }) => (
                <div key={id}>
                    <Link href={`/${id}`}>{title}</Link>
                </div>
            ))}
        </div>
    );
};

export const getServerSideProps: GetServerSideProps<THomeProps> = async (
    ctx,
) => {
    return { props: {} };
};

export default Home;
```

Great! Now we have a few pages that need some data. The only thing left is to supply this data. Let's explore the different ways we can do that.

### Using nest.js controller

Our `home` controller in `app.controller.ts` return an empty object. It turns out that everything that happens to be in that object will be accessible in `ctx.query` in GSSP.

Let's add some stub data in `app.service.ts`.

```typescript
// ./src/server/app.service.ts
import { Injectable } from '@nestjs/common';
import { from } from 'rxjs';

const BLOG_POSTS = [
    { title: 'Lorem Ipsum', id: 1 },
    { title: 'Dolore Sit', id: 2 },
    { title: 'Ame', id: 3 },
];

@Injectable()
export class AppService {
    getBlogPosts() {
        return from(BLOG_POSTS);
    }
}
```

In controller we may access this service and return the data.

```typescript
// ./src/server/app.controller.ts
import { map, toArray } from 'rxjs';

// ...

@Get('/')
@Render('index')
home() {
    return this.appService.getBlogPosts().pipe(
        toArray(),
        map((blogPosts) => ({ blogPosts })),
    );
}
```

Now we will have access to `blogPosts` property in `ctx.query` in GSSP. However it seems that this implementation is not very reliable: TypeScript should warn us, that there is in fact no `blogPosts` in `ctx.query`. It is typed to be `ParsedUrlQuery`.

Surely TypeScript is wrong? Let's leave a few `console.log`s of `ctx.query` in our `index.tsx` GSSP. Then open `localhost:3000`. Check our terminal (that's where the logs would land - GSSP is only run on the server). We indeed see that `blogPosts` are there. What's wrong then?

Let's open `localhost:3000/1` and then click the `Home` link. Suddenly our terminal logs an empty object. But how is that possible, we clearly return `blogPosts` property from our controller? 

> When navigating on the client side NEXT.js fetches an internal endpoint that executes _just_ the GSSP function and returns serialized JSON from it. So our `home` controller is not called at all and `ctx.query` is populated purely with path params and search query.

## Using direct services access

As stated previously, GSSP is only executed on the server. Therefore we could technically use nest.js services directly from inside GSSP.

This is in fact a very bad idea. You either have to construct every service yourself (then you have lots of repeated code and loose any profits from DI) or expose nest.js application with it's `get` method.

Still even if you were to bare with the spaghetti facilitated by global application access from different contexts you would end up missing the HTTP contexts when calling services.

## By sending requests to itself

Nothing really stops us from making asynchronous request using `fetch` from GSSP. However we should make a wrapper around `fetch` to choose which address to call. But before we proceed we have to get information about where the code is executed and what port the server is subscribed to.

```typescript
// ./src/shared/constants/env.ts
export const isServer = typeof window === 'undefined';

export const isClient = !isServer;

export const NODE_ENV = process.env.NODE_ENV;

export const PORT = process.env.PORT || 3000;
```

Now update port subscription in `main.ts` (`await app.listen(PORT)`) and choose NEXT.js mode depending on environment.

```javascript
// ./src/server/app.module.ts
RenderModule.forRootAsync(
    Next({ dev: NODE_ENV === 'development' }),
    { viewsDir: null }
)

// ./package.json
"start:dev": "NODE_ENV=development nest start --path ./tsconfig.server.json --watch"
```

> Now that server imports modules from `src/shared` the structure of compiled nest.js server differs from before. If you previously changed `entryFile` in `nest-cli.json` return it to the old value (`server/main.ts`), clean `dist` folder and reboot the server.

### Wrapper for fetch

Now we can add a wrapper for `fetch` to choose hostname depending on execution environment.

```typescript
// ./src/shared/utils/fetch.ts
import { isServer, PORT } from '../constants/env';

const envAwareFetch = (url: string, options?: Record<string, unknown>) => {
    const fetchUrl =
        isServer && url.startsWith('/') ? `http://localhost:${PORT}${url}` : url;

    return fetch(fetchUrl, options).then((res) => res.json());
};

export { envAwareFetch as fetch };
```

And update `app.service.ts`.

```typescript
// ./src/server/app.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { from, of, toArray } from 'rxjs';

const BLOG_POSTS = [
  { title: 'Lorem Ipsum', id: 1 },
  { title: 'Dolore Sit', id: 2 },
  { title: 'Ame', id: 3 },
];

@Injectable()
export class AppService {
  getBlogPosts() {
    return from(BLOG_POSTS).pipe(toArray());
  }

  getBlogPost(postId: number) {
    const blogPost = BLOG_POSTS.find(({ id }) => id === postId);

    if (!blogPost) {
      throw new NotFoundException();
    }

    return of(blogPost);
  }
}
```

Add new API endpoints to `app.controller.ts`.

```typescript
// ./src/server/app.controller.ts
import { Controller, Get, Param, ParseIntPipe, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('/')
  @Render('index')
  home() {
    return {};
  }

  @Get(':id')
  @Render('[id]')
  public blogPost(@Param('id') id: string) {
    return {};
  }

  @Get('/api/blog-posts')
  public listBlogPosts() {
    return this.appService.getBlogPosts();
  }

  @Get('/api/blog-posts/:id')
  public getBlogPostById(@Param('id', new ParseIntPipe()) id: number) {
    return this.appService.getBlogPost(id);
  }
}
```

Finally let's update GSSP methods.

```typescript
// ./src/pages/index.tsx
import { fetch } from 'src/shared/utils/fetch';

export const getServerSideProps: GetServerSideProps<THomeProps> = async () => {
    const blogPosts = await fetch('/api/blog-posts');
    return { props: { blogPosts } };
};

// ./src/pages/[id].tsx
import { fetch } from 'src/shared/utils/fetch';

export const getServerSideProps: GetServerSideProps<TBlogProps> = async () => {
    const id = ctx.query.id;
    const post = await fetch(`/api/blog-posts/${id}`);
    
    return { props: { post } };
};
```

Visit `localhost:3000`. Indeed the blog list is available. Let's visit one of the links to a post - everything should work here as well, client side navigation is fine.

However when we update the page on a post page we get an error - there is no such blog post. Everything seemed to work with client side navigation.

> As we already discovered `nest-next` puts controller return value to `ctx.query`. This means that the actual query is not there and it the responsibility of the user to prepare it.

To fix this issue we will return the id from `blogPost` controller.

```typescript
// ./src/server/app.controller.ts
@Get(':id')
@Render('[id]')
public blogPost(@Param('id') id: string) {
    return { id };
}
```

> API endpoint casted the parameter to an integer. In this case it's better to not parse parameters to stay consistent with NEXT.js and keep them as strings.

Now let's refresh the page in the browser - this should have solved our issue.

### Passing path parameters

Obviously we are at a terrible situation when we need to manually pass all the parameters in controllers. What if we need to use search parameters? Surely there is a way to fix that?

We will use a snippet of AOP [(Aspect-oriented programming)](https://en.wikipedia.org/wiki/Aspect-oriented_programming) and one of its mechanisms in nest.js: an [Interceptor](https://docs.nestjs.com/interceptors).

```typescript
// ./src/server/params.interceptor.ts
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ParamsInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
        const request = context.switchToHttp().getRequest() as Request;
          
      /* after executing the handler add missing request query and params */
        return next.handle().pipe(
            map((data) => {
                return {
                    ...request.query,
                    ...request.params,
                    ...data,
                };
            }),
        );
    }
}
```

> According to NEXT.js documentation path parameters take priority over search params. We also will not override the data from our handler.

Decorate page controller handlers with new interceptor.

```typescript
// ./src/server/app.controller.ts
import { UseInterceptors } from '@nestjs/common';
import { ParamsInterceptor } from './params.interceptor';

// ...

@Get('/')
@Render('index')
@UseInterceptors(ParamsInterceptor)
public home() {
    return {};
}

@Get(':id')
@Render('[id]')
@UseInterceptors(ParamsInterceptor)
public blogPost() {
    return {};
}
```

It is important that path parameters have the same names in nest.js and NEXT.js. In other words path params in `@Get` and in `@Render` should be the same. API endpoints must not have this interceptor - we do not want to return path params back when calling API.

> It would be wise to separate API and page controllers. Then we would be able to put `@UseInterceptors` decorator on the entire controller class. In this article for simplicity purposes API and page controllers are merged.

Let's validate our changes in the browser by refreshing the page. We should still properly see a blog post by id.

## Next steps

At this point we have a basic `nest-next` application capable of rendering pages and supplying them with data. However we are yet to capitalise on some of the real advantages of such a setup. There are also some other quirks you might encounter especially using this combo for enterprise development. 

![Short slightly artistic summary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxda50rmbujfd1jziofk.jpg)

To learn more advanced topics like HMR, more SSR techniques and proxying with `nest-next` read [the second part of this article](https://dev.to/yakovlev_alexey/advanced-practices-for-nestjs-nextjs-projects-36g9).

At this moment I hope this article helped you to finally try out those frameworks together with great efficiency despite the scarce info on actual `nest-next` usage in docs.