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.
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 productionstart
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.
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
.
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 compileddist/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.
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.
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);
Inside packages/server
, we install the necessary packages
yarn add @nestjs/graphql apollo-server-express graphql-tools graphql type-graphql
Very nice guide, but dotenv doesn't work in nuxt due to fs dependency. I have yet to try any workarounds yet