Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
NuxtJS / NestJS fullstack development

This is my take on fullstack development using NuxtJS and NestJS.

Development steps

  • Lerna monorepo
  • TypeScript linting and formatting
  • Minimal NestJS server
  • Minimal NuxtJS client
  • Environment configuration
  • TypeGraphQL
  • TypeORM / MySQL
  • HTTP proxy middleware
  • Apollo Client
  • User datamodel, queries and mutations
  • JWT / local authentication
  • Server-side authorization guards
  • Buefy / SCSS
  • Form handling and validation
  • Login / Registration / Profile forms

We assume basic knowledge of NodeJS.

Lerna monorepo

We first install Lerna globally, initialize a new git repository and turn it into a monorepo with independent versioning:

npm install --global lerna
git init nuxtnest && cd nuxtnest
lerna init --independent

Let's adjust lerna.json so we can benefit from Yarn workspaces:

{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true
}

We’ll also adjust package.json to rename our project and define where the Yarn workspaces are located:

{
  "name": "nuxtnest",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev": "lerna run dev --stream --no-bail",
    "build": "lerna run build --stream --no-bail",
    "start": "lerna run start --stream --no-bail"
  },
  "devDependencies": {
    "lerna": "^3.18.3"
  }
}

As you can see, we also added some scripts:

  • dev compiles and executes the project in development mode (watch for changes and recompiles on-the-fly)
  • build compiles the packages for production
  • start executes the compiled project (build must be run before)

We should probably create .gitignore with sensible defaults:

.env
.nuxt
dist
node_modules
*.lock
*.log

We are now ready to configure our project for TypeScript development.

TypeScript linting and formatting

We first install TypeScript globally:

yarn add -WD typescript

A common practice with TypeScript monorepos is to have a shared tsconfig.json at the root of the project. In this file, we want to configure our TypeScript defaults like declaration, source map, etc.

{
  "compilerOptions": {
    "declaration": true,
    "sourceMap": true,
    "skipLibCheck": true
  }
}

This file should be included in every package's tsconfig.json with the following:

"extends": "../../tsconfig.json"

Now let's install and configure ESLint and Prettier:

yarn add -WD eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -WD prettier eslint-config-prettier eslint-plugin-prettier

Then we configure Prettier with the .prettierrc file:

{
  "tabWidth": 2,
  "trailingComma": "es5",
  "semi": true,
  "singleQuote": true
}

...and ESLint with the .eslintrc file:

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "prettier",
    "prettier/@typescript-eslint",
    "plugin:prettier/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module"
  },
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "off"
  }
}

Finally, we add the global lint script to package.json:

"lint": "eslint --ext .js,.ts --ignore-path .gitignore --fix packages/"

As a bonus, we can configure VSCode to automatically fix the code whenever a file is saved. This is achieved by installing the ESLint extension and setting ESLint: Run to onSave.

Minimal NestJS server

Let's create our server package:

mkdir -p packages/server && cd packages/server
yarn init -py

We use a manual NestJS configuration (without using the Nest CLI) for greater control, using ts-node-dev as our build tool:

yarn add @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs
yarn add -D ts-node-dev

Again, let's tweak the server's package.json for our needs:

  • Rename the package with the @nuxtnest/ prefix
  • Set main to the compiled dist/main.js javascript file
  • Add our dev / build / start scripts
{
  "name": "@nuxtnest/server",
  "version": "1.0.0",
  "private": true,
  "main": "dist/main.js",
  "scripts": {
    "dev": "ts-node-dev --no-notify --transpileOnly src/main.ts",
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "dependencies": {
    "@nestjs/common": "^6.10.11",
    "@nestjs/core": "^6.10.11",
    "@nestjs/platform-express": "^6.10.11",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^6.5.3"
  },
  "devDependencies": {
    "ts-node-dev": "^1.0.0-pre.44"
  }
}

Before we can start writing TypeScript code inside src/, we have to create the server's tsconfig.json with the following in mind:

  • Don't forget to extend our shared tsconfig.json
  • NestJS requires decorator metadata
  • Compiled TS code should be in dist/
  • TS source code should reside in src/
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist"
  },
  "include": [
    "src"
  ]
}

Our minimal server consists of a single, empty application module that is used to bootstrap NestJS from the main file.

Let's create the two corresponding files:

// src/app.module.ts
import { Module } from '@nestjs/common';

@Module({})
export class AppModule { }
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(4000);
}
bootstrap();

We should now be able to start our server using the previously defined scripts:

# Compile, execute and watch for changes (development)
yarn dev

# Compile and execute (production)
yarn build && yarn start

Navigating to http://localhost:4000 shoud bring up the standard 404 response from NestJS.

Minimal NuxtJS client

Let's create our client package:

mkdir packages/client && cd packages/client
yarn init -py

We install NuxtJS with TypeScript runtime / build support:

yarn add nuxt @nuxt/typescript-runtime
yarn add -D @nuxt/typescript-build

We then configure TypeScript tsconfig.json:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "target": "es2018",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

... and NuxtJS nuxt.config.ts:

import { Configuration } from '@nuxt/types';

const nuxtConfig: Configuration = {
  buildModules: ['@nuxt/typescript-build'],
};

export default nuxtConfig;

Finally we adjust package.json with the name prefix and scripts:

{
  "name": "@nuxtnest/client",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "start": "nuxt-ts start"
  },
  "dependencies": {
    "@nuxt/typescript-runtime": "^0.3.3",
    "nuxt": "^2.11.0"
  },
  "devDependencies": {
    "@nuxt/typescript-build": "^0.5.2"
  }
}

We won't create anything inside pages/ for now as we only want to have a working minimal NuxtJS client.

Environment configuration

One of the advantages of using a monorepo is sharing code between packages, which can prove useful for defining common configuration values or secrets.

Let's create a shared package for this purpose:

mkdir packages/shared && cd packages/shared
yarn init -py

Like before, we create tsconfig.json to configure TypeScript:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": [
    "src"
  ]
}

... and adjust package.json:

{
  "name": "@nuxtnest/shared",
  "version": "1.0.0",
  "main": "dist/index.js",
  "private": true,
  "scripts": {
    "dev": "tsc -w",
    "build": "tsc"
  }
}

We use TypeScript watch mode (tsc -w) for dev and we don't define a start script since this package is not to be executed.

Also, since we might need to share code for other purposes, we'll use src/index.ts as an entry point for the package and export envconfig.ts from there:

// src/index.ts
export * from './envconfig';

We want our environment configuration to have the following features:

  • Type-safe via interface EnvConfig
  • Values stored inside the .env file at the root of the project
  • Values can be overridden by system environment variables
  • Sensible default values whenever possible (required otherwise)
  • Throw an error when a required value is missing

We'll use dotenv and env-var to help us achieve this:

yarn add dotenv env-var
yarn add -D @types/dotenv

As a starting point, let's define client / server port numbers. For such a configuration, our envconfig.ts file would look something like this:

// src/envconfig.ts
import * as env from 'env-var';
import { config } from 'dotenv';
import { resolve } from 'path';

config({ path: resolve(__dirname, '../../../.env') });

export interface EnvConfig {
  clientPort: number;
  serverPort: number;
}

export const envConfig: EnvConfig = {
  clientPort: env.get('CLIENT_PORT', '3000').asPortNumber(),
  serverPort: env.get('SERVER_PORT', '4000').asPortNumber(),
};

To use our new package from elsewhere in the project, we first need to link the dependencies using Lerna (from the root of the project):

lerna bootstrap

Then we can import from anywhere using:

import { envConfig } from '@nuxtnest/shared';
console.log(envConfig.serverPort);

TypeGraphQL

Inside packages/server, we install the necessary packages

yarn add @nestjs/graphql apollo-server-express graphql-tools graphql type-graphql
@wallopthecat

This comment has been minimized.

Copy link

wallopthecat commented Jun 30, 2020

Very nice guide, but dotenv doesn't work in nuxt due to fs dependency. I have yet to try any workarounds yet

@lewebsimple

This comment has been minimized.

Copy link
Owner Author

lewebsimple commented Jun 30, 2020

@wellopthecat You can import the env config inside nuxt.config.ts and pass the values to the client (and server) at build time.

@wallopthecat

This comment has been minimized.

Copy link

wallopthecat commented Jun 30, 2020

Thanks, very smart. The new way to do this in Nuxt is using these 2 props in nuxt.config

  publicRuntimeConfig: {},
  privateRuntimeConfig: {},

See https://nuxtjs.org/blog/moving-from-nuxtjs-dotenv-to-runtime-config

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.