Combining Nuxt.js and NestJS into a single application

Nuxt is a server side rendering framework for Vue. Nest is a Spring-like framework for Node. In this article, I would like to talk about how you can combine two frameworks to build a scalable web application.

At the end of this article, you should be able to build an application which:

  1. can be built and deployed to a single port in a single instance.
  2. you can start the Nuxt and the Nest separately in local for development.
  3. you can start the whole application as one in development mode locally for pre-deployment testing.

I am going to start with a new Nuxt and a new Nest project, but this guide also works using existing projects.

File Structure & Configuration

First of all, we need to update the default file structure and config for both project to avoid any potential conflict after combination.

Nest

  1. Rename the src folder to server
  2. Update sourceRoot in nest-cli.json to “server
  3. Add “include” in tsconfig.build.json with value “[“server/**/*”]
  4. Update outDir in tsconfig.json to “./dist/server

Nuxt

  1. Add a new folder named client
  2. Move all your source code into client folder
  3. In nuxt.config.js, add attribute srcDir with value “client/
  4. In nuxt.config.js, add attribute server with value { port: 8080 } (any port different with the port of your Nest project)
  5. In nuxt.config.js, update “export default” to “module.exports =” if your Nest project doesn’t support the export default syntax

Combine Two Projects and Update Dependencies

What we need to do next is to simply put two projects into a same folder and merge the dependencies.

  1. Backup your Nest project
  2. Copy client and nuxt.config.js from your Nuxt project to your Nest project
  3. Merge the package.json (excludes scripts) of your Nuxt project into the one in Nest
{
"name": "nestnuxt",
"version": "1.0.0",
"scripts": {
},
"dependencies": {
"@nestjs/common": "^6.7.2",
"@nestjs/core": "^6.7.2",
"@nestjs/platform-express": "^6.7.2",
"nuxt": "^2.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.0",
"rxjs": "^6.5.3"
},
"devDependencies": {
"@nestjs/cli": "^6.9.0",
"@nestjs/schematics": "^6.7.0",
"@types/express": "^4.17.1",
"@types/node": "^12.7.5",
"ts-loader": "^6.1.1",
"ts-node": "^8.4.1",
"tsconfig-paths": "^3.9.0",
"typescript": "^3.6.3"
}
}

This will be how it looks if you use the default starter projects. For simplicity, I have removed all the unnecessary modules, such as tslint, etc.

Checkpoint

If you don’t feel safe, you may test if your merging is correct by adding these two scripts in package.json.

{
"scripts": {
"nuxt": "nuxt",
"nest": "nest start"
}
}

npm run nuxt should start your nuxt project normally, while npm run nest for your nest project. (Remember to npm install first)

Adding Nuxt Instance into Nest

The logic is very straight forward. First, we create a controller in Nest specifically for Nuxt. In that controller, we maintain a Nuxt instance. When there is request to our Nest, we will first check if there is any other controller matching the path. When there is no matching controller, we pass the request to Nuxt controller and let the Nuxt instance to handle the rest.

Nuxt Controller

import { Controller, Get, Request, Response } from "@nestjs/common";import { Builder, Nuxt } from "nuxt";
import * as config from "../nuxt.config.js";
@Controller()
export class NuxtController {
nuxt;
constructor() {
if (process.env.mode === "production") {
config.dev = false;
this.nuxt = new Nuxt(config);
}
else if (process.env.IS_NUXT_ENABLED) {
this.nuxt = new Nuxt(config);
new Builder(this.nuxt).build();
}
}
@Get('*')
async root(
@Request() req,
@Response() res
) {
if (this.nuxt) {
await this.nuxt.render(req, res);
}
else {
res.send('Nuxt is disabled.');
}
}
}

Create nuxt.controller.ts and import it in app.module.ts. One hack here is that when you declare the controllers in app.module.ts, NuxtController must be declared at last. It is because the NuxtController is using a wild card to all route. It is going to override all get method which is declared after NuxtController.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { NuxtController } from './nuxt.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [
AppController,
NuxtController //Put this at the last, so that Nest will check for all other controller first before going to Nuxt
],
providers: [AppService],
})
export class AppModule {}

If you are using the default project starter, you need to update the path in app.controller.ts as it is eating the root path.

Adding Build and Run Scripts

Run the following commands to install all essential packages for making the scripts:

npm install --save-dev copyfiles@2.1.0 cross-env@5.2.0 nodemon@1.18.10 npm-run-all@4.1.5

Add nodemon for Nest development

Add nodemon.json in your folder with the following content:

{
"watch": ["server"],
"ext": "ts",
"exec": "ts-node -r tsconfig-paths/register server/main.ts"
}

The nest-cli also provides a watch function to watch code change, so I believe we don’t really need nodemon here. Yet, I couldn’t find much document about how I can exclude file from the watch function. Thus, I use nodemon as a workaround.

Scripts for Development

{
"scripts": {
"dev": "cross-env IS_NUXT_ENABLED=true nodemon",
"dev:client": "nuxt",
"dev:server": "nodemon"
}
}
  • npm run dev: start both Nest and Nuxt as one
  • npm run dev:client: start the Nuxt, with hot reloading on all Nuxt code
  • npm run dev:server: start the Nest, with watching on all Nest code

Scripts for Deployment

{
"scripts: {
"build": "run-s clean:dist compile:server compile:client copy:.nuxt copy:client copy:config",
"clean:dist": "rimraf dist",
"compile:server": "tsc -p tsconfig.build.json",
"compile:client": "cross-env mode=production nuxt build",
"copy:client": "copyfiles -a \"client/**/*\" dist",
"copy:.nuxt": "copyfiles -a \".nuxt/**/*\" dist",
"copy:config": "copyfiles nuxt.config.js package.json package-lock.json dist"
}
}

Full package.json

{
"name": "nuxt",
"version": "1.0.0",
"scripts": {
"dev": "cross-env IS_NUXT_ENABLED=true nodemon",
"dev:client": "nuxt",
"dev:server": "nodemon",
"build": "run-s clean:dist compile:server compile:client copy:.nuxt copy:client copy:config",
"clean:dist": "rimraf dist",
"compile:server": "tsc -p tsconfig.build.json",
"compile:client": "cross-env mode=production nuxt build",
"copy:client": "copyfiles -a \"client/**/*\" dist",
"copy:.nuxt": "copyfiles -a \".nuxt/**/*\" dist",
"copy:config": "copyfiles nuxt.config.js package.json package-lock.json dist"
},
"dependencies": {
"@nestjs/common": "^6.7.2",
"@nestjs/core": "^6.7.2",
"@nestjs/platform-express": "^6.7.2",
"nuxt": "^2.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.0",
"rxjs": "^6.5.3"
},
"devDependencies": {
"@nestjs/cli": "^6.9.0",
"@nestjs/schematics": "^6.7.0",
"@types/express": "^4.17.1",
"@types/node": "^12.7.5",
"copyfiles": "^2.1.0",
"cross-env": "^5.2.0",
"nodemon": "^1.18.10",
"npm-run-all": "^4.1.5",
"ts-loader": "^6.1.1",
"ts-node": "^8.4.1",
"tsconfig-paths": "^3.9.0",
"typescript": "^3.6.3"
}
}

Deployment

After running npm run build, the following folder will be generated:

In order to start in production, you can run node server/main.js after running npm install. For the source code, you may check my GitHub.

(#Updated at 2020/2/8) In order to deploy it to your production environment, you will need to add the environment variable mode with value production to toggle off the Nuxt development mode. You may change the condition process.env.mode === "production" in the NuxtController if you want.

Web developer from Hong Kong. Most interested in Angular and Vue. Currently working on a Nuxt.js + NestJS project.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store