I really enjoy working with both Nuxt.js (vue.js SSR framework) and Nest.js (node.js server-side framework). I wanted to see if I could get the two working together in one application. I spent a lot of time trying different ways of getting this to work. I will go over the solution I came up with and walk you through step by step on how to put the two in one project. The solution is not perfect, but should work well enough. I would only recommend this for smaller applications. If you are working on a larger application, I would recommend that you keep the two separate.
# Create app
First we want to start off by creating a new nuxt.js project using npx
.
$ npx create-nuxt-app nuxtjs-nestjs-integration
You will be prompted with a series of questions on how to create your project. You can choose what you want to include in the project but just make sure to select Typescript
, Axios
, ESLint
, Prettier
and Jest
. Here are the settings I chose.
create-nuxt-app v3.6.0
✨ Generating Nuxt.js project in nuxtjs-nestjs-integration
? Project name: nuxtjs-nestjs-integration
? Programming language: TypeScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? Continuous integration: None
? Version control system: Git
When I created the project, I chose to include eslint and prettier, so I am going to update the .prettierrc
file to expect semicolons at the end of lines and turn off trailing commas. Just my own personal preference.
// .prettierrc
{
"trailingComma": "none",
"semi": true,
"arrowParens": "always",
"singleQuote": true
}
I ran the lint command and fixed all the issues from my changes for prettier.
$ npm run lint
# Move app to client directory
We need to separate the nuxt and nest code so we will create a client
directory and move the following directories inside of it.
- assets
- components
- layouts
- middleware
- pages
- plugins
- static
- store
Move the Logo.spec.js
file to client/components
and remove the test
directory. Make sure to update the import inside Logo.spec.js
.
// client/components/Logo.spec.js
import Logo from './Logo.vue';
Next, we will configure the nuxt app to know that all the nuxt directories are in 'client'. Add the following to nuxt.config.js
// nuxt.config.js
srcDir: 'client/'
# Nuxt Decorators
Before we start adding in nest.js, we need to install the libraries that add decorator support to our nuxt.js project. Here is a list of libraries we will be adding:
- nuxt-property-decorator (opens new window) - used to define vue components, props, watches, etc.
If we look at the index.vue
page, we will import nuxt-property-decorator
which uses the Component
decorator to define a Vue component. Inside the @Component
decorator, I added the name
attribute and called this view Index
. Also notice how you define a class called Index
as the default export and extend from Vue
from the nuxt-property-decorator
library.
$ npm install nuxt-property-decorator --save
Update index.vue
as follows:
// pages/index.vue
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
@Component({
name: 'Index'
})
export default class Index extends Vue {}
</script>
# Adding Nest.js
To add nest.js, we will first create a shell project that we can copy the base structure from. Run the following commands to create a bare bones nest.js project.
$ npm i -g @nestjs/cli
$ nest new project-name
Looking at the generated package.json
, there are some dependencies that we can install into our project.
$ npm i --save @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs
Lets do the same for the development dependencies.
npm i --save-dev @nestjs/cli @nestjs/schematics @nestjs/testing @types/jest @types/supertest supertest
Now we need to create a server
directory and copy over files from nest project in src
and test
directories.
server
--src
--app.controller.spec.ts
--app.controller.ts
--app.module.ts
--app.service.ts
--main.ts
--test
--app.e2e-spec.ts
--jest-e2e.json
We will also copy nest-cli.json
over to the root directory of the project. Update nest-cli.json
so that it points to the server
directory.
// nest-cli.json
{
"collection": "@nestjs/schematics",
"sourceRoot": "server/src",
"root": "server"
}
Nest.js has some extra eslint settings that we need to copy over. Add the following rules to .eslintrc.js
.
// .eslintrc.js
rules: {
'no-useless-constructor': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
Trying to create a tsconfig.json that works for both nuxt and nest proves to be to problematic so we are going to create a separate tsconfig for nest. Create a file named tsconfig-server.json
in the root directory. We will set the output directory to be .nest
which follows the naming convention for the nuxt output directory which is .nuxt
.
// tsconfig-server.json
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./.nest",
"baseUrl": "./",
"incremental": true
}
}
Copy over tsconfig.build.json
and update the path of the server tsconfig file.
// tsconfig.build.json
{
"extends": "./tsconfig-server.json",
"exclude": [
"node_modules",
".nuxt",
".nest",
"client",
"**/*spec.ts"
]
}
Lastly, we need to update the tsconfig.json
file which will be used for the client code to include some types and exclude some directories.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": [
"ESNext",
"ESNext.AsyncIterable",
"DOM"
],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
],
"@/*": [
"./*"
]
},
"types": [
"@nuxt/types",
"@nuxtjs/axios",
"@types/node",
"@types/jest",
"@types/supertest"
]
},
"exclude": [
"node_modules",
".nuxt",
".nest",
"server"
]
}
# Testing
Before we worry about getting the app up and running, lets go ahead and get the nuxt unit tests, nest unit tests and nest e2e tests running. I first tried to get all the unit tests working together and realized it was more trouble than it was worth so I've separated them out.
We will start by adding two coverage directories to our .gitignore
file. I will elaborate more on this shortly. Also include the build directory for nest.js.
// .gitignore
coverage-client
coverage-server
# nest.js build output
.nest
# Client tests
To get the nuxt tests working, we need to update rootDir
and coverageDirectory
in jest.config.js
. The rootDir
needs to point to the client
directory and the coverageDirectory
will point to coverage-client
so we can separate it from the server coverage report.
// jest.config.js
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'^~/(.*)$': '<rootDir>/$1',
'^vue$': 'vue/dist/vue.common.js'
},
moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
rootDir: 'client',
transform: {
'^.+\\.ts$': 'ts-jest',
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest'
},
collectCoverage: true,
coverageDirectory: '../coverage-client',
collectCoverageFrom: [
'<rootDir>/components/**/*.vue',
'<rootDir>/pages/**/*.vue'
]
};
Update the test
npm script to test:client
.
// package.json
"test:client": "jest"
Run the client tests to make sure they work.
$ npm run test:client
# Server Tests
For the nest.js tests, we have to do a little more work. Create a jest-server.config.js
file to hold the configuration for the nest.js tests. Just like the nuxt tests, we need to update rootDir
and coverageDirectory
in jest-server.config.js
. The rootDir
needs to point to the server
directory and the coverageDirectory
will point to coverage-server
so we can separate it from the client coverage report. You also need to specify the globals
property so you can point to the server tsconfig file.
module.exports = {
globals: {
'ts-jest': {
tsConfig: './tsconfig-server.json'
}
},
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'server',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest'
},
collectCoverage: true,
collectCoverageFrom: ['**/*.(t|j)s', '!**/*.e2e-spec.(t|j)s'],
coverageDirectory: '../coverage-server',
testEnvironment: 'node'
};
Now lets add a npm command to run the server tests.
// package.json
"test:server": "jest --config ./jest-server.config.js"
Run the server tests to make sure they work.
$ npm run test:server
# Server e2e Tests
For the server e2e tests, we need to configure the e2e jest config to point to the correct tsconfig file.
// server/test/jest-e2e.json
{
"globals": {
"ts-jest": {
"tsConfig": "tsconfig-server.json"
}
},
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
Now lets add a npm command to run the server e2e tests.
// package.json
"test:e2e": "jest --config ./server/test/jest-e2e.json"
Run the server e2e tests to make sure they work.
$ npm run test:e2e
# All tests
Add a npm command to run all the tests.
// package.json
"test": "npm run test:client && npm run test:server && npm run test:e2e"
Run all the tests to make sure they work.
$ npm run test
# Formatting and Linting
This part is pretty trivial. We need to update the lint:js
npm script to include the server directory and we need to copy over the format
npm script from the nest.js application and update it to include the client code.
// package.json
"lint:js": "eslint --ext \".ts,.js,.vue\" --ignore-path .gitignore .",
"lint": "npm run lint:js",
"format": "prettier --write \"server/**/*.ts\" \"client/**/*.(js|ts|vue)\"",
Now you can run lint
and format
for both the client and server code.
$ npm run format
$ npm run lint
# Development and Production builds
The last thing we have to do is get the application up and running in development and production modes. This is where it gets a little interesting. For development mode, we will have to run the client on one port and the server on another port. I was unable to get the two working together so that the hot code replacement would work with the nest.js code. During development, I want to be able to make changes to the code and have it redeployed automatically. For the production build, we will bundle everything up so that everything can run on the same port.
# Development
We will start with updating the nest.js main.ts
file to define the path (/api
) and port (4000
) for development mode.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('/api');
await app.listen(4000);
}
bootstrap();
Now update the nuxt.config.js
file to define the default Axios URL for the nest.js API.
// nuxt.config.js
axios: {
baseURL: 'http://localhost:4000/api'
},
Lets go ahead and add the API call to the home page. We'll add an asyncData
method call to return the value from the API call and and a hello
variable to display Hello World
on the home page. We use asyncData
here to make the API call on the server and not the client. If you chose to use the nuxt application as a SPA, you should use the mounted
hook here instead.
// client/pages/index.vue
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
import { Context } from '@nuxt/types';
@Component({
name: 'Index'
})
export default class Index extends Vue {
private hello!: string;
async asyncData({ $axios }: Context) {
let hello;
try {
hello = await $axios.$get('/');
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
return {
hello
};
}
}
</script>
Output the return value from the API by adding this to the end of the template.
<!-- client/pages/index.vue -->
<div>{{ hello }}</div>
Lastly, we will add another npm script to run the nest.js server in development mode.
// package.json
"dev:server": "nest start --debug --watch",
Now, for development mode, run the following two npm scripts in two different terminal windows.
$ npm run dev:server
$ npm run dev
Run the app on http://localhost:3000 (opens new window) and verify that you see Hello World!
on the home page.
# Production
Now lets get into the production build. We will need a separate entry point for the nest.js server than the main.ts
file. Go ahead and create a nest.ts
file in the server
directory. This version is a little different than main.ts
in that we don't specify the port or global prefix. That will be configured within the nuxt.config.js
file.
// server/nest.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './src/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.init();
return app.getHttpAdapter().getInstance();
}
export default bootstrap;
Now lets concentrate on the nuxt.config.js
file. We'll use process.env.NODE_ENV
to determine whether we are building for production or development mode. We will need to make a few changes here.
- Import the
nest.js
file that will be compiled from thenest.ts
file we just created. We'll update the build npm script soon to achieve this. - In development mode, we will run nest.js on port
4000
and port3000
in production mode (same as nuxt.js). Feel free to run on whatever port you want. - We will define
serverMiddleware
when we are building in production mode to run the API's. You can find more about theserverMiddleware
property here (opens new window). - We'll use async/await to wait for the nest.js API's to be able to bootstrap
// nuxt.config.js
import bootstrap from './.nest/nest.js';
const isDev = process.env.NODE_ENV === 'development';
const config = async () => ({
srcDir: 'client/',
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'nuxtjs-nestjs-integration',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' }
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
serverMiddleware: isDev ? [] : [{ path: '/api', handler: await bootstrap() }],
// Global CSS: https://go.nuxtjs.dev/config-css
css: [],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/typescript
'@nuxt/typescript-build'
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
// https://go.nuxtjs.dev/axios
'@nuxtjs/axios'
],
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {
baseURL: isDev ? 'http://localhost:4000/api' : 'http://localhost:3000/api'
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {}
});
export default config;
We just have to update our build npm script to first compile the nest.js code prior to building the nuxt.js code.
// package.json
"build": "nest build && nuxt build"
Now, for production mode, run the following npm scripts.
$ npm run build
$ npm run start
Run the app on http://localhost:3000 (opens new window) and verify that you see Hello World!
on the home page.
I hope this saves some of you some time trying to figure this out. You can find the example source code here (opens new window).