NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' course

Github Repositories

Source code for the NestJs and Nuxt version of the Full-Stack Vue with GraphQL - The Ultimate Guide Udemy course

Within the code you can see how to:

  • Use NestJs with GraphQL and MongoDb to create powerful back ends applicaction.
  • Use Apollo Boost to create powerful full-stack apps.
  • Handle errors on the client and server with Apollo / GraphQL
  • Implement session-based JWT authentication to GraphQL applications
  • Integrate Apollo with Nuxt Vuex for more reliable and scalable state management
  • Implement infinite scrolling functionality using Vue-Apollo
  • Deploy full-stack JavaScript / GraphQL applications using Heroku.
  • Learn how to write queries and mutations in the GraphQL language on both the client and server
  • Make use of many useful MongoDB methods and features
  • Be able to create attractive, sophisticated UIs using the Vuetify CSS framework
  • Become more familiar with all the best ES6 / 7 features such as async / await, destructuring, spread operators, arrow functions, etc
  • Create a multi-language application using nuxt-i18n

Table of contents

You can find more information on Full-Stack Vue with GraphQL - The Ultimate Guide.

This is the Nuxt version of the Full-Stack Vue with GraphQL - The Ultimate Guide Udemy course.

Section 3-4: Setting up the Server project.

I.-Create the main folder for the solution

  • Create the main full-stack-vue-with-graphql-the-ultimate-guide-nuxt folder
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs
$ mkdir full-stack-vue-with-graphql-the-ultimate-guide-nuxt

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs
$ cd full-stack-vue-with-graphql-the-ultimate-guide-nuxt

  • Initialize Git
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt
$ git init
Initialized empty Git repository in C:/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/.git/
  • Copy the README.md and .gitignore from the original full-stack-vue-with-graphql-the-ultimate-guide project.

II.-Create the server project using NetsJS

  • We are now going to use the NestJS CLI to scaffold the project. Nest is a progressive Node.js framework for building efficient, reliable and scalable server-side applications.

  • We need to ensure NestJs CLI is already installed.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ nest -V
6.5.0
  • If it is not installed we can install it by using npm i -g @nestjs/cli
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/Typescript
$ npm i -g @nestjs/cli
C:\Users\juan.pablo.perez\AppData\Roaming\npm\nest -> C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\@nestjs\cli\bin\nest.js
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\@nestjs\cli\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ @nestjs/cli@6.5.0
added 255 packages from 174 contributors in 55.819s
  • We are going to create the server project using the NestJS CLI
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ nest new server
⚡  We will scaffold your app in a few seconds..

CREATE /server/.prettierrc (51 bytes)
CREATE /server/nest-cli.json (84 bytes)
CREATE /server/nodemon-debug.json (163 bytes)
CREATE /server/nodemon.json (67 bytes)
CREATE /server/package.json (1802 bytes)
CREATE /server/README.md (3370 bytes)
CREATE /server/tsconfig.build.json (97 bytes)
CREATE /server/tsconfig.json (325 bytes)
CREATE /server/tslint.json (426 bytes)
CREATE /server/src/app.controller.spec.ts (617 bytes)
CREATE /server/src/app.controller.ts (274 bytes)
CREATE /server/src/app.module.ts (249 bytes)
CREATE /server/src/app.service.ts (142 bytes)
CREATE /server/src/main.ts (208 bytes)
CREATE /server/test/app.e2e-spec.ts (561 bytes)
CREATE /server/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? npm
▹▹▹▸▹ Installation in progress... ☕

�  Successfully created project server
�  Get started with the following commands:

$ cd server
$ npm run start


                          Thanks for installing Nest �
                 Please consider donating to our open collective
                        to help us maintain this package.


               �  Donate: https://opencollective.com/nest
  • We can test if it has been installed properly by executing npm run start
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ cd server

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm run start

> server@0.0.1 start C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server
> ts-node -r tsconfig-paths/register src/main.ts

[Nest] 22508   - 06/17/2019, 5:20 AM   [NestFactory] Starting Nest application...
[Nest] 22508   - 06/17/2019, 5:20 AM   [InstanceLoader] AppModule dependencies initialized +23ms
[Nest] 22508   - 06/17/2019, 5:20 AM   [RoutesResolver] AppController {/}: +13ms
[Nest] 22508   - 06/17/2019, 5:20 AM   [RouterExplorer] Mapped {/, GET} route +7ms
[Nest] 22508   - 06/17/2019, 5:20 AM   [NestApplication] Nest application successfully started +8ms
  • Browse http://localhost:3000/

  • Let's see what has been created.

III.-Setting up Configuration (using environment variables)

  • We are going to use dotenv, so we need to install the dotenv and @types/dotenv.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i dotenv
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ dotenv@8.0.0
added 1 package and audited 22998 packages in 21.757s
found 62 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details
$ npm i --save-dev @types/dotenv
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ @types/dotenv@6.1.1
added 1 package from 4 contributors and audited 23000 packages in 15.297s
found 62 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details

V.-Setting up MongoDb

  • As we can see on NestJS Mongo we have to install the @nestjs/mongoose and mongoose packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm install --save @nestjs/mongoose mongoose
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ mongoose@5.6.0
+ @nestjs/mongoose@6.1.2
added 19 packages from 12 contributors and audited 22997 packages in 23.456s
found 62 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details
  • We also need to install the @types/mongoose for the dev environment
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i --save-dev @types/mongoose
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ @types/mongoose@5.5.6
added 3 packages from 47 contributors and audited 23006 packages in 16.131s
found 62 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details
  • We are going to have the URI connection in a .dev document that dotenv will use.

.dev

MONGO_URI=mongodb+srv://USERNAME:PASSWORD@CLUSTERID.mongodb.net/DATABASENAME?retryWrites=true&w=majority
  • We are going to create the src/config folder with the config.module.ts and config.service.ts documents. It will be used to manage our config variables.

src/config/config.service.ts

import * as dotenv from "dotenv";
import * as fs from "fs";

export class ConfigService {
  MONGODB_URI: string;
  private readonly envConfig: { [key: string]: string };

  constructor() {
    if (
      process.env.NODE_ENV === "production" ||
      process.env.NODE_ENV === "staging"
    ) {
      this.envConfig = {
        MONGO_URI: process.env.MONGO_URI
      };
    } else {
      this.envConfig = dotenv.parse(fs.readFileSync(".env"));
    }
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

src/config/config.module.ts

import { Module } from "@nestjs/common";
import { ConfigService } from "./config.service";

@Module({
  providers: [ConfigService],
  exports: [ConfigService]
})
export class ConfigModule {}
  • We are going to create the src/database folder with the database.providers.ts and database.module.ts documents that are going to be used to set up MongoDb

src/config/database.providers.ts

import { ConfigModule } from "../config/config.module";
import { ConfigService } from "../config/config.service";
import { MongooseModule } from "@nestjs/mongoose";

export const databaseProviders = [
  MongooseModule.forRootAsync({
    imports: [ConfigModule],
    inject: [ConfigService],
    useFactory: async (config: ConfigService) => ({
      uri: config.get("MONGO_URI"),
      useNewUrlParser: true
    })
  })
];

src/config/database.module.ts

import { Module } from "@nestjs/common";
import { databaseProviders } from "./database.providers";

@Module({
  imports: [...databaseProviders],
  exports: [...databaseProviders]
})
export class DatabaseModule {}
  • We also need to modify the app.module.ts document to import the DatabaseModule.

app.module.ts

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";

@Module({
  imports: [DatabaseModule],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}
  • We can test if everything is working without issues.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm run start:dev

> server@0.0.1 start:dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server
> concurrently --handle-input "wait-on dist/main.js && nodemon" "tsc -w -p tsconfig.build.json"

[1]

[1]
6:40:04 PM - Starting compilation in watch mode...
[1]
[0] [nodemon] 1.19.1
[0] [nodemon] to restart at any time, enter `rs`
[0] [nodemon] watching: C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\dist/**/*
[0] [nodemon] starting `node dist/main`
[0] [nodemon] restarting due to changes...
[1]
[1] 6:40:09 PM - Found 0 errors. Watching for file changes.
[0] [nodemon] restarting due to changes...
[0] [nodemon] starting `node dist/main`
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [NestFactory] Starting Nest application...
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [InstanceLoader] DatabaseModule dependencies initialized +49ms
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [InstanceLoader] MongooseModule dependencies initialized +2ms
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [InstanceLoader] ConfigModule dependencies initialized +2ms
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [InstanceLoader] AppModule dependencies initialized +12ms
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [InstanceLoader] MongooseCoreModule dependencies initialized +866ms
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [RoutesResolver] AppController {/}: +7ms
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [RouterExplorer] Mapped {/, GET} route +3ms
[0] [Nest] 15368   - 06/20/2019, 6:40 PM   [NestApplication] Nest application successfully started +3ms

IV.-Setting up GraphQL with Apollo Server

  • As we can see on NestJs GraphQL Quick Start we need to install the @nestjs/graphql, apollo-server-express, graphql-tools and graphql packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i --save @nestjs/graphql apollo-server-express graphql-tools graphql

> core-js@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\apollo-env\node_modules\core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> protobufjs@6.8.8 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\protobufjs
> node scripts/postinstall


> type-graphql@0.17.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\type-graphql
> node ./dist/postinstall || exit 0

npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ graphql@14.3.1
+ graphql-tools@4.0.4
+ apollo-server-express@2.6.3
+ @nestjs/graphql@6.2.4
added 150 packages from 149 contributors and audited 22927 packages in 36.49s
found 62 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details
  • We are going to apply Code First approach for managing GraphQL with NestJS

  • We need to install the type-graphql package

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i type-graphql

> type-graphql@0.17.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\type-graphql
> node ./dist/postinstall || exit 0

npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ type-graphql@0.17.4
updated 1 package and audited 22957 packages in 15.604s
found 62 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details
  • We need to modify src\app.module.ts document to register GraphQLModule and to add autoSchemaFile property to the options object. As we have chosen the Code First approach, the GraphQL schema is going to be generated automatically by NestJS. schema.gpl is the name of the schema file that is going to be created.

  • We are going to get rid of the app.controller.ts document as we are not going to use controllers but GrahpQL.

src\app.service.ts

import { Injectable } from "@nestjs/common";

@Injectable()
export class AppService {}

src\app.module.ts

import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";
import { GraphQLModule } from "@nestjs/graphql";

@Module({
  imports: [
    DatabaseModule,
    GraphQLModule.forRoot({
      autoSchemaFile: "schema.gql"
    })
  ],
  providers: [AppService]
})
export class AppModule {}

VII.Create the User model.

  • database schema: This is an organization of data as a blueprint for defining the structure and the types of data that the database needs to store.

  • data transfer object: This is an object that defines how data will be sent over the network and carries the data between processes.

  • interfaces: TypeScript interfaces are used for type-checking. It can be used to define the types of data that should be passed for an application.

1.Use the nestjs-typegoose package to avoid having to create a schema and an interface

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i typegoose  nestjs-typegoose
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ nestjs-typegoose@5.2.1
+ typegoose@5.7.2
added 3 packages from 3 contributors and audited 23011 packages in 26.646s
found 62 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details

2.Create the model folder and the model documents.

  • Create the users folder and inside it the model folder. In this folder we are going to put the documents related to the database users colection. The user.model.ts document must be created with the definition of the User model.

src/users/model/users.model.ts

import * as mongoose from "mongoose";
import { prop, Typegoose } from "typegoose";
import { IsString, IsDate, IsArray } from "class-validator";

export class User extends Typegoose {
  @IsString()
  @prop({ required: true, unique: true, trim: true })
  username: string;

  @IsString()
  @prop({ required: true, trim: true })
  email: string;

  @IsString()
  @prop({ required: true, trim: true })
  password: string;

  @IsString()
  avatar?: string;

  @IsDate()
  @prop({ default: Date.now })
  joinDate: Date;

  @IsArray()
  @prop({ required: true, ref: "Post" })
  favorites: [mongoose.Schema.Types.ObjectId];
}
  • We are going to create the dtos folder where we are going to put the Data Transfer Objects used to send data to the collections. We are going to create the create-user.dto.ts document that will be used to create a new user.

src/users/model/dto/create-user.dto.ts

export class CreateUserDto {
  readonly username: string;
  readonly email: string;
  readonly password: string;
}

3.Create the grahpql folder with the graphql documents

  • We need to create graphql folder with the inputs and types subfolders.

  • We need to create the users.resolver.ts with the GraphQL resolver for the user.

src/users/graphql/users.resolver.ts

import { Resolver, Query, Mutation, Args } from "@nestjs/graphql";
import { UsersService } from "../users.service";
import { UserInput } from "./inputs/user.input";
import { User } from "./types/user.type";

@Resolver()
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Query(() => [User])
  async getUsers() {
    return await this.usersService.getUsers();
  }

  @Mutation(() => User)
  async signupUserWithInput(@Args("input") input: UserInput) {
    return await this.usersService.signupUserWithInput(input);
  }
  @Mutation(() => User)
  async signupUser(
    @Args("username") username: string,
    @Args("email") email: string,
    @Args("password") password: string
  ) {
    return await this.usersService.signupUser({ username, email, password });
  }
}
  • The inputs subfolder will contain the classes used to define the graphQL inputs used by Queries and Mutations. We are going to create the user.input.ts document that will be used to create a new user when they sign up.

users//inputs/user.input.ts

import { InputType, Field, Int } from "type-graphql";

@InputType()
export class UserInput {
  @Field()
  readonly username: string;
  @Field()
  readonly email: string;
  @Field()
  readonly password: string;
}
  • The types subfolder will containt the classes used to define the types returned by the graphQL Queries and Mutations. We are going to create the user.type.ts document that will be used to return the data from the User collection.

users//types/user.type.ts

import { Field, ObjectType, ID } from "type-graphql";

@ObjectType()
export class User {
  @Field(() => ID)
  readonly _id: string;
  @Field()
  readonly username: string;
  @Field()
  readonly email: string;
  @Field()
  readonly password: string;
  @Field({ nullable: true })
  readonly avatar: string;
  @Field()
  readonly joinDate: Date;
  @Field(() => [ID], { nullable: true })
  readonly favorites: [string];
}

4.Create the users service

  • We are going to use the NestJs CLI to create the users service.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate service users
CREATE /src/users/users.service.spec.ts (453 bytes)
CREATE /src/users/users.service.ts (89 bytes)
UPDATE /src/users/users.module.ts (159 bytes)

src/users/users.service.ts

import { Injectable } from "@nestjs/common";

@Injectable()
export class UsersService {}

src/users/users.service.spec.ts

import { Test, TestingModule } from "@nestjs/testing";
import { UsersService } from "./users.service";

describe("UsersService", () => {
  let service: UsersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersService]
    }).compile();

    service = module.get<UsersService>(UsersService);
  });

  it("should be defined", () => {
    expect(service).toBeDefined();
  });
});

src/users/users.module.ts

import { Module } from "@nestjs/common";

@Module()
export class UsersModule {}
  • We need to modify the new service to define the methods used to manage the access to the users collection.

src/users/users.service.ts

import { Injectable } from "@nestjs/common";
import { InjectModel } from "nestjs-typegoose";
import { User } from "./model/user.model";
import { CreateUserDto } from "./model/dtos/create-user.dto";
import { ModelType } from "typegoose";

@Injectable()
export class UsersService {
  constructor(@InjectModel(User) private readonly userModel: ModelType<User>) {}

  async getUsers(): Promise<User[] | null> {
    return await this.userModel.find().exec();
  }

  async signupUserWithInput(createUserDto: CreateUserDto): Promise<User> {
    const username = createUserDto.username;
    const user = await this.userModel.findOne({ username });
    if (user) {
      throw new Error("User already exists");
    }
    const createdUser = new this.userModel(createUserDto);
    return await createdUser.save();
  }

  async signupUser({ username, email, password }): Promise<User> {
    const user = await this.userModel.findOne({ username });
    if (user) {
      throw new Error("User already exists");
    }
    const createdUser = new this.userModel({ username, email, password });
    return await createdUser.save();
  }
}

5.Create the users module

  • We are going to use the NestJs CLI to create the users module.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate module users
CREATE /src/users/users.module.ts (82 bytes)
UPDATE /src/app.module.ts (438 bytes)

src/users/users.module.ts

import { Module } from "@nestjs/common";

@Module()
export class UsersModule {}
  • We need to modify the module created to define the imports and provides used for Users

src/users/users.module.ts

import { Module } from "@nestjs/common";
import { TypegooseModule } from "nestjs-typegoose";
import { User } from "./model/user.model";
import { UsersService } from "./users.service";
import { UsersResolver } from "./graphql/users.resolver";

@Module({
  imports: [TypegooseModule.forFeature([User])],
  providers: [UsersService, UsersResolver]
})
export class UsersModule {}

6.-Modify the app.module.ts

  • We need to modify the app.module.ts to include the UsersModule and the GraphQLModule.

src/app.module.ts

import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";
import { GraphQLModule } from "@nestjs/graphql";
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    DatabaseModule,
    UsersModule,cd ..
    GraphQLModule.forRoot({
      autoSchemaFile: "schema.gql"
    }),
  ],
  providers: [AppService]
})
export class AppModule { }

7.-Examples

request

{
  getUsers {
    _id
    username
    email
    password
    avatar
    joinDate
    favorites
  }
}

response

{
  "data": {
    "getUsers": [
      {
        "_id": "5d04cf30deb8673e8c38fc2d",
        "username": "John",
        "email": "john@gmail.com",
        "password": "Password",
        "avatar": null,
        "joinDate": "2019-06-15T10:57:52.586Z",
        "favorites": null
      },
      {
        "_id": "5d0da3d023e0c02b9c90ef2a",
        "username": "Paul",
        "email": "paul@gmail.com",
        "password": "Password",
        "avatar": null,
        "joinDate": "2019-06-22T03:43:12.472Z",
        "favorites": null
      },
      {
        "_id": "5d0da5312ce4d742386276af",
        "username": "Sarah",
        "email": "sarah@gmail.com",
        "password": "Password",
        "avatar": null,
        "joinDate": "2019-06-22T03:49:05.357Z",
        "favorites": null
      },
      {
        "_id": "5d0da82b9b5def3308ad242a",
        "username": "Mary",
        "email": "mary@gmail.com",
        "password": "Password",
        "avatar": null,
        "joinDate": "2019-06-22T04:01:47.521Z",
        "favorites": null
      },
      {
        "_id": "5d0e10127dd57444d060778e",
        "username": "Jess",
        "email": "jess@gmail.com",
        "password": "Password",
        "avatar": null,
        "joinDate": "2019-06-22T11:25:06.222Z",
        "favorites": null
      },
      {
        "_id": "5d0e10297dd57444d060778f",
        "username": "Juan",
        "email": "juan@gmail.com",
        "password": "Password",
        "avatar": null,
        "joinDate": "2019-06-22T11:25:29.692Z",
        "favorites": null
      }
    ]
  }
}

request

mutation {
  signupUser(username: "Juan", email: "juan@gmail.com", password: "Password") {
    _id
    username
    email
    avatar
    password
    joinDate
  }
}

response

{
  "data": {
    "signupUser": {
      "_id": "5d0e10297dd57444d060778f",
      "username": "Juan",
      "email": "juan@gmail.com",
      "avatar": null,
      "password": "Password",
      "joinDate": "2019-06-22T11:25:29.692Z"
    }
  }
}

request

mutation {
  signupUserWithInput(
    input: { username: "Jess", email: "jess@gmail.com", password: "Password" }
  ) {
    _id
    username
    email
    avatar
    password
    joinDate
  }
}

response

{
  "data": {
    "signupUserWithInput": {
      "_id": "5d0e10127dd57444d060778e",
      "username": "Jess",
      "email": "jess@gmail.com",
      "avatar": null,
      "password": "Password",
      "joinDate": "2019-06-22T11:25:06.222Z"
    }
  }
}

VI.-Create the Post model.

1.-Create the posts folder with a module and a `service.

  • We need to create the usr/posts folder and use the NestJs CLI to create the module and the service
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate module posts
CREATE /src/posts/posts.module.ts (82 bytes)
UPDATE /src/app.module.ts (576 bytes)
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate service posts
CREATE /src/posts/posts.service.spec.ts (453 bytes)
CREATE /src/posts/posts.service.ts (89 bytes)
UPDATE /src/posts/posts.module.ts (507 bytes)
  • The src/app.module.ts document is updated automatically
import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";
import { GraphQLModule } from "@nestjs/graphql";
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [
    DatabaseModule,
    GraphQLModule.forRoot({
      autoSchemaFile: "schema.gql"
    }),
    UsersModule,
    PostsModule,
  ],
  providers: [AppService]
})
export class AppModule { }

2.-Create the model folder and the model documents.

  • We need to create the src/posts/model and src/posts/model/dtos folders and the post.model.ts and create-post.dto.ts documents.

src/posts/model/post.model.ts

import * as mongoose from "mongoose";
import { prop, Typegoose } from 'typegoose';
import { IsString, IsDate, IsArray, IsInt } from 'class-validator';
import { User } from '../../users/model/user.model'

class Message {
  @IsString()
  @prop({ required: true })
  messageBody: string;

  @IsDate()
  @prop({ default: Date.now })
  messageDate: Date;

  @IsString()
  @prop({ required: true, ref: User })
  messageUser: mongoose.Schema.Types.ObjectId
}

export class Post extends Typegoose {
  @IsString()
  @prop({ required: true })
  title: string; 

  @IsString()
  @prop({ required: true })
  imageUrl: string;

  @IsString()
  @prop({ required: true })
  categories: string[];

  @IsString()
  @prop({ required: true })
  description: string;

  @IsDate()
  @prop({ default: Date.now })
  createdDate: Date

  @IsInt()
  @prop({ default: 0 })
  likes: number;

  @IsString()
  @prop({ required: true, ref: User })
  createdBy: mongoose.Schema.Types.ObjectId

  @IsArray()
  @prop({ ref: Message })
  messages: [Message]
}

src/posts/model/dtos/create-post.dto.ts

export class CreatePostDto {
  readonly title: string;
  readonly imageUrl: string;
  readonly categories: string[];
  readonly description: string;
  readonly createdBy: string;
}

3.-Create the grahpql folder with the graphql documents.

  • We need to create the src/posts/graphql, src/posts/graphql/inputs and src/posts/graphql/types folders and the users.resolver.ts, post.input.ts and post.type.ts documents.

src/posts/graphql/post.resolver.ts

import { Resolver, Query, Mutation, Args } from "@nestjs/graphql";
import { PostsService } from "../posts.service";
import { PostInput } from "./inputs/post.input";
import { Post } from "./types/post.type";

@Resolver()
export class PostsResolver {
  constructor(private readonly postsService: PostsService) {}

  @Query(() => [Post])
  async getPosts() {
    return await this.postsService.getPosts();
  }

  @Mutation(() => Post)
  async addPostWithInput(@Args("input") input: PostInput) {
    const createdBy = input.creatorId;
    const { title, imageUrl, categories, description } = input;
    return await this.postsService.addPost({
      title,
      imageUrl,
      categories,
      description,
      createdBy
    });
  }
  @Mutation(() => Post)
  async addPost(
    @Args("title") title: string,
    @Args("imageUrl") imageUrl: string,
    @Args({ name: "categories", type: () => [String] }) categories: [string],
    @Args("description") description: string,
    @Args("creatorId") creatorId: string
  ) {
    const createdBy = creatorId;
    return await this.postsService.addPost({
      title,
      imageUrl,
      categories,
      description,
      createdBy
    });
  }
}

src/posts/graphql/inputs/post.input.ts

import { InputType, Field, ID } from 'type-graphql';

@InputType()
export class PostInput {
  @Field()
  readonly title: string;
  @Field()
  readonly imageUrl: string;
  @Field(() => [String], { nullable: "items" })
  readonly categories: string[];
  @Field()
  readonly description: string;
  @Field(() => ID)
  readonly creatorId: string;

}

src/posts/graphql/inputs/post.type.ts

import { Field, ObjectType, ID, Int } from 'type-graphql';
import { User } from '../../../users/graphql/types/user.type'

@ObjectType()
class Message {
  @Field(() => ID)
  readonly _id: string  
  @Field()
  readonly messageBody: string;
  @Field()
  readonly messageDate: Date;
  @Field()
  readonly messageUser: string;
}

@ObjectType()
export class Post {
  @Field(() => ID)
  readonly _id: string
  @Field()
  readonly title: string;
  @Field()
  readonly imageUrl: string;
  @Field(() => [String], { nullable: "items" })
  readonly categories: string[];
  @Field()
  readonly description: string;
  @Field()
  readonly createdDate: Date
  @Field(() => Int, { nullable: true })
  likes: number;
  @Field(() => User)
  readonly createdBy: User
  @Field(() => [Message], { nullable: true })
  readonly messages: [Message]
}

4.-Modify the module and service documents.

  • We need to modify the module created to define the imports and provides used for posts

src/users/posts.module.ts

import { Module } from "@nestjs/common";
import { TypegooseModule } from "nestjs-typegoose";
import { Post } from "./model/post.model";
import { PostsService } from "./posts.service";
import { PostsResolver } from "./graphql/posts.resolver";

@Module({
  imports: [TypegooseModule.forFeature([Post])],
  providers: [PostsService, PostsResolver]
})
export class PostsModule {}
  • We need to modify the service to define the methods used to manage the access to the posts collection.

src/users/posts.service.ts

import { Injectable } from "@nestjs/common";
import { InjectModel } from "nestjs-typegoose";
import { Post } from "./model/post.model";
import { CreatePostDto } from "./model/dtos/create-post.dto";
import { ModelType } from "typegoose";

@Injectable()
export class PostsService {
  constructor(@InjectModel(Post) private readonly postModel: ModelType<Post>) {}

  async getPosts(): Promise<Post[] | null> {
    const posts = await this.postModel
      .find({})
      .sort({ createdDate: "desc" })
      .populate({
        path: "createdBy",
        model: "User"
      });
    return posts;
  }

  async addPost(createPostDto: CreatePostDto): Promise<Post> {
    const newPost = new this.postModel(createPostDto);
    return await newPost.save();
  }
}

5.-Examples

request

{
  getPosts {
    _id
    title
    imageUrl
    categories
    description
    createdDate
    likes
    createdBy {
      _id
      username
      email
      avatar
      password
      joinDate
    }
  }
}

response

{
  "data": {
    "getPosts": [
      {
        "_id": "5d0e0fdc7dd57444d060778d",
        "title": "Credenza",
        "imageUrl": "https://images.crateandbarrel.com/is/image/Crate/ClybournIICredenza3QF16/?$web_zoom_furn_av$&180802085137&wid=1008&hei=567",
        "categories": ["Furniture"],
        "description": "A piece of furniture I want to buy",
        "createdDate": "2019-06-22T11:24:12.972Z",
        "likes": 0,
        "createdBy": {
          "_id": "5d0da3d023e0c02b9c90ef2a",
          "username": "Paul",
          "email": "paul@gmail.com",
          "avatar": null,
          "password": "Password",
          "joinDate": "2019-06-22T03:43:12.472Z"
        }
      },
      {
        "_id": "5d04d377deb8673e8c38fc2f",
        "title": "Mona lisa",
        "imageUrl": "https://images.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.readingpublicmuseum.org%2Fexhibit_secrets-of-mona-lisa-4.jpg&f=1",
        "categories": ["Art"],
        "description": "A painting",
        "createdDate": "2019-06-15T11:16:07.195Z",
        "likes": 0,
        "createdBy": {
          "_id": "5d04cf30deb8673e8c38fc2d",
          "username": "John",
          "email": "john@gmail.com",
          "avatar": null,
          "password": "Password",
          "joinDate": "2019-06-15T10:57:52.586Z"
        }
      }
    ]
  }
}

request

mutation {
  addPost(
    title: "Credenza"
    imageUrl: "https://images.crateandbarrel.com/is/image/Crate/ClybournIICredenza3QF16/?$web_zoom_furn_av$&180802085137&wid=1008&hei=567"
    categories: ["Furniture"]
    description: "A piece of furniture I want to buy"
    creatorId: "5d0da3d023e0c02b9c90ef2a"
  ) {
    title
    imageUrl
    categories
    description
    createdDate
    likes
  }
}

response

{
  "data": {
    "addPost": {
      "title": "Credenza",
      "imageUrl": "https://images.crateandbarrel.com/is/image/Crate/ClybournIICredenza3QF16/?$web_zoom_furn_av$&180802085137&wid=1008&hei=567",
      "categories": ["Furniture"],
      "description": "A piece of furniture I want to buy",
      "createdDate": "2019-06-22T11:24:12.972Z",
      "likes": 0
    }
  }
}

request

mutation {
  addPostWithInput(
    input: {
      title: "Tasty coffee"
      imageUrl: "https://images.pexels.com/photos/374757/pexels-photo-374757.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
      categories: ["Art", "Food"]
      description: "Some nice coffee artwork"
      creatorId: "5d0da3d023e0c02b9c90ef2a"
    }
  ) {
    title
    imageUrl
    categories
    description
    createdDate
    likes
  }
}

response

{
  "data": {
    "addPostWithInput": {
      "title": "Tasty coffee",
      "imageUrl": "https://images.pexels.com/photos/374757/pexels-photo-374757.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
      "categories": ["Art", "Food"],
      "description": "Some nice coffee artwork",
      "createdDate": "2019-06-22T11:27:58.416Z",
      "likes": 0
    }
  }
}

Section 5: Create Vue Frontend with NuxtJs

I.Create the NuxtJs app using the create-nuxt-app CLI

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npx create-nuxt-app client
npx: installed 379 in 37.171s
> Generating Nuxt.js project in C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client
? Project name client
? Project description Frontend for the Nuxt version of [Full-Stack Vue with GraphQL - The Ultimate Guide
? Use a custom server framework none
? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier
? Use a custom UI framework vuetify
? Use a custom test framework jest
? Choose rendering mode Universal
? Author name Juan Pablo Perez
? Choose a package manager npm
\ Installing packages with npm
 Choose a package manager npm

> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"

> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"


> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)

> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0

npm

> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0


> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0

Love nodemon? You can now support the project via the open collective:
  > https://opencollective.com/nodemon/donate

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})



> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0

Love nodemon? You can now support the project via the open collective:
  > https://opencollective.com/nodemon/donate


> client@1.0.0 lint C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client
> eslint --ext .js,.vue --ignore-path .gitignore . "--fix"


  To get started:

        cd client
        npm run dev

  To build & start for production:

        cd client
        npm run build
        npm start

  To test:

        cd client
        npm run test
  • Run de app to check if it works properly
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm run dev

> client@1.0.0 dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client
> nuxt


   ╭─────────────────────────────────────────────╮
   │                                             │
   │   Nuxt.js v2.8.1                            │
   │   Running in development mode (universal)   │
   │                                             │
   │   Listening on: http://localhost:3000/      │
   │                                             │
   ╰─────────────────────────────────────────────╯

i Preparing project for development                                                                                                                                                             16:34:20
i Initial build may take a while                                                                                                                                                                16:34:20
√ Builder initialized                                                                                                                                                                           16:34:21
√ Nuxt files generated                                                                                                                                                                          16:34:21

√ Client
  Compiled successfully in 14.51s

√ Server
  Compiled successfully in 11.43s


 ERROR  (node:13864) DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks instead                                                                                            16:34:25

i Waiting for file changes                                                                                                                                                                      16:34:40
i Memory usage: 235 MB (RSS: 337 MB)
  • Browse to http://localhost:3000/

II.Create a script to run server and client at the same time

  • We are going to run server and client at the same time by using the concurrently package.

  • We need to create a new empty package.json file with npm init -y at the main folder.

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm init -y
Wrote to C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\package.json:

{
  "name": "full-stack-vue-with-graphql-the-ultimate-guide-nuxt",
  "version": "1.0.0",
  "description": "Source code for the NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' Udemy course.",  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/issues"
  },
  "homepage": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt#readme"
}

  • We need to install the concurrently package.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm i --save-dev concurrently
npm notice created a lockfile as package-lock.json. You should commit this file.
+ concurrently@4.1.0
added 86 packages from 48 contributors and audited 103 packages in 21.691s
found 0 vulnerabilities
  • Change the server/src/main.ts to run the server on port 4000

    server/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(process.env.PORT || 4000);
}
bootstrap();
  • Modify the package.json document to create the scripts needed to run the server and the client at the same time

package.json

{
  "name": "full-stack-vue-with-graphql-the-ultimate-guide-nuxt",
  "version": "1.0.0",
  "description": "Source code for the NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' Udemy course.",
  "main": "index.js",
  "scripts": {
    "server": "cd server && npm run start:dev",
    "client": "cd client && npm run dev",
    "dev": "concurrently --names \"server,client\" \"npm run server --silent\" \"npm run client --silent\""
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/issues"
  },
  "homepage": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt#readme",
  "devDependencies": {
    "concurrently": "^4.1.0"
  }
}

  • Execute the npm run dev script.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm run dev

> full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt
> concurrently --names "server,client" "npm run server --silent" "npm run client --silent"


5:16:48 PM - Starting compilation in watch mode...
[server] [1]
[server] [0] [nodemon] 1.19.1
[server] [0] [nodemon] to restart at any time, enter `rs`
[server] [0] [nodemon] watching: C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\dist/**/*
[server] [0] [nodemon] starting `node dist/main`
[client] i Listening on: http://localhost:3000/
[client] i Preparing project for development
[client] i Initial build may take a while
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [NestFactory] Starting Nest application...
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] AppModule dependencies initialized +97ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] DatabaseModule dependencies initialized +2ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] TypegooseModule dependencies initialized +1ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] ConfigModule dependencies initialized +2ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] GraphQLModule dependencies initialized +24ms
[server] [1] node_modules/apollo-server-express/dist/ApolloServer.d.ts(1,8): error TS1192: Module '"C:/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server/node_modules/@types/express/index"' has no default export.
[server] [1] node_modules/apollo-server-express/dist/ApolloServer.d.ts(2,8): error TS1192: Module '"C:/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server/node_modules/@types/cors/index"' has no default export.
[server] [1]
[server] [1] 5:17:13 PM - Found 2 errors. Watching for file changes.
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] TypegooseCoreModule dependencies initialized +1552ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] TypegooseModule dependencies initialized +2ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] TypegooseModule dependencies initialized +2ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] UsersModule dependencies initialized +3ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [InstanceLoader] PostsModule dependencies initialized +2ms
[server] [0] [Nest] 17600   - 06/22/2019, 5:17 PM   [NestApplication] Nest application successfully started +641ms
[client] √ Builder initialized
[client] √ Nuxt files generated
[client] i Compiling Client
[client] i Compiling Server


  • Browse to http://localhost:3000/ to see the client

  • Browse to http://localhost:4000/graphql to see the server graphql

III.-Set up the Vuetify Theme

  • Modify the nuxt.config.js document to change the vuetify.theme to use the same colours as the ones from the 'original` Vue app.

/client/nuxt.config.js

// import colors from 'vuetify/es5/util/colors'

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js modules
   */
  modules: ['@nuxtjs/vuetify', '@nuxtjs/pwa', '@nuxtjs/eslint-module'],
  /*
   ** vuetify module configuration
   ** https://github.com/nuxt-community/vuetify-module
   */
  vuetify: {
    theme: {
      // primary: colors.blue.darken2,
      // accent: colors.grey.darken3,
      // secondary: colors.amber.darken3,
      // info: colors.teal.lighten1,
      // warning: colors.amber.base,
      // error: colors.deepOrange.accent4,
      // success: colors.green.accent3
      primary: '#3B125F',
      secondary: '#8B5FBF',
      accent: '#BF653F',
      error: '#722530',
      warning: '#A37513',
      info: '#396893',
      success: '#4caf50'
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}

IV.-Create the Pages (and remove the test code)

  • Modify the client/layouts/default.vue document to create the layout based on the original App.vue document.

client/layouts/default.vue

<template>
  <v-app style="background: #E3E3EE">
    <!-- Side Navbar -->
    <v-navigation-drawer v-model="sideNav" fixed app temporary>
      <v-toolbar color="accent" dark flat>
        <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
        <nuxt-link to="/" tag="span" style="cursor: pointer">
          <h1 class="title pl-3">VueShare</h1>
        </nuxt-link>
      </v-toolbar>

      <v-divider></v-divider>

      <!-- Side Navbar Links -->
      <v-list>
        <v-list-tile
          v-for="(item, i) in sideNavItems"
          :key="i"
          :to="item.link"
          ripple
        >
          <v-list-tile-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            {{ item.title }}
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>

    <!-- Horizontal Navbar -->
    <v-toolbar fixed color="primary" dark>
      <!-- App Title -->
      <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link to="/" tag="span" style="cursor: pointer">
          VueShare
        </nuxt-link>
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <!-- Search Input -->
      <v-text-field
        flex
        prepend-icon="search"
        placeholder="Search posts"
        color="accent"
        single-line
        hide-details
      ></v-text-field>

      <v-spacer></v-spacer>

      <!-- Horizontal Navbar Links -->
      <v-toolbar-items class="hidden-xs-only">
        <v-btn
          v-for="(item, i) in horizontalNavItems"
          :key="i"
          :to="item.link"
          flat
        >
          <v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
          {{ item.title }}
        </v-btn>
      </v-toolbar-items>
    </v-toolbar>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      sideNav: false
    }
  },
  computed: {
    horizontalNavItems() {
      return [
        { icon: 'chat', title: 'Posts', link: '/posts' },
        { icon: 'lock_open', title: 'Sign In', link: '/signin' },
        { icon: 'create', title: 'Sign Up', link: '/signup' }
      ]
    },
    sideNavItems() {
      return [
        { icon: 'chat', title: 'Posts', link: '/posts' },
        { icon: 'lock_open', title: 'Sign In', link: '/signin' },
        { icon: 'create', title: 'Sign Up', link: '/signup' }
      ]
    }
  },
  methods: {
    toggleSideNav() {
      this.sideNav = !this.sideNav
    }
  }
}
</script>

  • Modify the client/pages/index.vue document copy the content from the original /components/Home.vue

client/pages/index.vue

<template>
  <v-container>
    <h1>Home</h1>
  </v-container>
</template>

<script>
export default {
  name: 'Home'
}
</script>

  • Create the post folder with the add.vue and index.vue document.

client/pages/posts/add.vue

<template>
  <v-container>
    <h1>Add Post</h1>
  </v-container>
</template>

<script>
export default {
  name: 'AddPost'
}
</script>

client/pages/posts/index.vue

<template>
  <v-container>
    <h1>Posts</h1>
  </v-container>
</template>

<script>
export default {
  name: 'Posts'
}
</script>

  • Create the profile folder with the index.vue document.

client/pages/profile/index.vue

<template>
  <v-container>
    <h1>Profile</h1>
  </v-container>
</template>

<script>
export default {
  name: 'Profile'
}
</script>

  • Create the signin folder with the index.vue document.

client/pages/signin/index.vue

<template>
  <v-container>
    <h1>Signin</h1>
  </v-container>
</template>

<script>
export default {
  name: 'Signin'
}
</script>

  • Create the signup folder with the index.vue document.

client/pages/signup/index.vue

<template>
  <v-container>
    <h1>Signup</h1>
  </v-container>
</template>

<script>
export default {
  name: 'Signup'
}
</script>

  • Check if the application works the same way as the original application.

V.-Sep up Typescript

  • We are going to follow the TypeScript Support documentation to set up the use of Typescript with our Nuxt application

1.Install the needed Typescript packages

  • We need to install @nuxt/typescript in devDependencies and ts-node in dependencies
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i -D @nuxt/typescript
npm WARN deprecated @types/chokidar@2.1.3: This is a stub types definition. chokidar provides its own type definitions, so you do not need this installed.
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fork-ts-checker-webpack-plugin\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ @nuxt/typescript@2.8.1
added 55 packages from 162 contributors and audited 884211 packages in 72.918s
found 0 vulnerabilities
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i ts-node
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fork-ts-checker-webpack-plugin\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ ts-node@8.3.0
added 5 packages from 4 contributors and audited 884219 packages in 54.323s
found 0 vulnerabilities

2.-Create the tsconfig.json document

  • We need to create an empty tsconfig.json file in the root project folder.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ touch tsconfig.json

' Note: The tsconfig.json file will automatically update with default values the first time we run the nuxt command.

3.-rename the nuxt.config.js document to nuxt.config.ts and modify it.

  • To use TypeScript in the configuration file, we need to rename nuxt.config.js to nuxt.config.ts and change the document.

nuxt.config.ts

// import colors from 'vuetify/es5/util/colors'
import NuxtConfiguration from '@nuxt/config'

const config: NuxtConfiguration = {

  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js modules
   */
  modules: ['@nuxtjs/vuetify', '@nuxtjs/pwa', '@nuxtjs/eslint-module'],
  /*
   ** vuetify module configuration
   ** https://github.com/nuxt-community/vuetify-module
   */
  vuetify: {
    theme: {
      // primary: colors.blue.darken2,
      // accent: colors.grey.darken3,
      // secondary: colors.amber.darken3,
      // info: colors.teal.lighten1,
      // warning: colors.amber.base,
      // error: colors.deepOrange.accent4,
      // success: colors.green.accent3
      primary: '#3B125F',
      secondary: '#8B5FBF',
      accent: '#BF653F',
      error: '#722530',
      warning: '#A37513',
      info: '#396893',
      success: '#4caf50'
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}

export default config;

4.-Set up Linting with ESLint

  • We need to install @typescript-eslint/eslint-plugin and @typescript-eslint/parser.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fork-ts-checker-webpack-plugin\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ @typescript-eslint/parser@1.10.2
+ @typescript-eslint/eslint-plugin@1.10.2
added 8 packages from 5 contributors and audited 884247 packages in 64.087s
found 0 vulnerabilities
  • We need to edit the ESLint configuration .eslintrc.js document by adding the @typescript-eslint plugin and making @typescript-eslint/parser the default parser.

.eslintrc.js (before changes)

module.exports = {
  
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {
    parser: 'babel-eslint'
  },
  extends: [
    '@nuxtjs',
    'plugin:nuxt/recommended',
    'plugin:prettier/recommended',
    'prettier',
    'prettier/vue'
  ],
  plugins: [
    'prettier'
  ],
  // add your custom rules here
  rules: {
    "vue/component-name-in-template-casing": ["error", "PascalCase"],
    "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"    
  },
 
}

.eslintrc.js (after changes)

module.exports = {
  plugins: ['@typescript-eslint','prettier'],
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs',
    'plugin:nuxt/recommended',
    'plugin:prettier/recommended',
    'prettier',
    'prettier/vue'
  ],
  // add your custom rules here
  rules: {
    '@typescript-eslint/no-unused-vars': 'error',
    "vue/component-name-in-template-casing": ["error", "PascalCase"],
    "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"    
  },
 
}

  • Finally, add or edit the lint script of the package.json document.

. package.json

{
  "name": "client",
  "version": "1.0.0",
  "description": "Frontend for the Nuxt version of [Full-Stack Vue with GraphQL - The Ultimate Guide",
  "author": "Juan Pablo Perez",
  "private": true,
  "scripts": {
    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
    "precommit": "npm run lint",
    "test": "jest",
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "@nuxtjs/pwa": "^2.6.0",
    "@nuxtjs/vuetify": "0.5.5",
    "nuxt": "^2.0.0",
    "nuxt-i18n": "^5.12.7",
    "ts-node": "^8.3.0"
  },
  "devDependencies": {
    "@nuxt/typescript": "^2.8.1",
    "@nuxtjs/eslint-config": "^0.0.1",
    "@nuxtjs/eslint-module": "^0.0.1",
    "@typescript-eslint/eslint-plugin": "^1.10.2",
    "@typescript-eslint/parser": "^1.10.2",
    "@vue/test-utils": "^1.0.0-beta.27",
    "babel-core": "7.0.0-bridge.0",
    "babel-eslint": "^10.0.1",
    "babel-jest": "^24.1.0",
    "eslint": "^5.15.1",
    "eslint-config-prettier": "^4.1.0",
    "eslint-config-standard": ">=12.0.0",
    "eslint-plugin-import": ">=2.16.0",
    "eslint-plugin-jest": ">=22.3.0",
    "eslint-plugin-node": ">=8.0.1",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-plugin-prettier": "^3.0.1",
    "eslint-plugin-promise": ">=4.0.1",
    "eslint-plugin-standard": ">=4.0.0",
    "eslint-plugin-vue": "^5.2.2",
    "jest": "^24.1.0",
    "nodemon": "^1.18.9",
    "prettier": "^1.16.4",
    "stylus": "^0.54.5",
    "stylus-loader": "^3.0.2",
    "vue-jest": "^3.0.3"
  }
}

5.-Modify the pages to use TypeScript

  • We need to install the vue-property-decorator package that is going to help using TypeScripts in Vue documents.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm i vue-property-decorator
npm WARN full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 scripts['server'] should probably be scripts['start'].

+ vue-property-decorator@8.2.1
added 3 packages from 2 contributors and audited 106 packages in 6.878s
found 0 vulnerabilities

layouts/default.vue

<template>
  <v-app style="background: #E3E3EE">
    <!-- Side Navbar -->
    <v-navigation-drawer v-model="sideNav" fixed app temporary>
      <v-toolbar color="accent" dark flat>
        <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
        <nuxt-link to="/" tag="span" style="cursor: pointer">
          <h1 class="title pl-3">VueShare</h1>
        </nuxt-link>
      </v-toolbar>

      <v-divider></v-divider>

      <!-- Side Navbar Links -->
      <v-list>
        <v-list-tile
          v-for="(item, i) in sideNavItems"
          :key="i"
          :to="item.link"
          ripple
        >
          <v-list-tile-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            {{ item.title }}
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>

    <!-- Horizontal Navbar -->
    <v-toolbar fixed color="primary" dark>
      <!-- App Title -->
      <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link to="/" tag="span" style="cursor: pointer">
          VueShare
        </nuxt-link>
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <!-- Search Input -->
      <v-text-field
        flex
        prepend-icon="search"
        placeholder="Search posts"
        color="accent"
        single-line
        hide-details
      ></v-text-field>

      <v-spacer></v-spacer>

      <!-- Horizontal Navbar Links -->
      <v-toolbar-items class="hidden-xs-only">
        <v-btn
          v-for="(item, i) in horizontalNavItems"
          :key="i"
          :to="item.link"
          flat
        >
          <v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
          {{ item.title }}
        </v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
  </v-app>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator'

export default class Layout extends Vue {
  sideNav: boolean = false

  // computed
  get horizontalNavItems() {
    return [
      { icon: 'chat', title: 'Posts', link: '/posts' },
      { icon: 'lock_open', title: 'Sign In', link: '/signin' },
      { icon: 'create', title: 'Sign Up', link: '/signup' }
    ]
  }

  get sideNavItems() {
    return [
      { icon: 'chat', title: 'Posts', link: '/posts' },
      { icon: 'lock_open', title: 'Sign In', link: '/signin' },
      { icon: 'create', title: 'Sign Up', link: '/signup' }
    ]
  }

  // methods
  toggleSideNav() {
    this.sideNav = !this.sideNav
  }
}
</script>

layout/error.vue (It hasn't been changed)

<template>
  <v-app dark>
    <h1 v-if="error.statusCode === 404">
      {{ pageNotFound }}
    </h1>
    <h1 v-else>
      {{ otherError }}
    </h1>
    <NuxtLink to="/">
      Home page
    </NuxtLink>
  </v-app>
</template>

<script>
export default {
  layout: 'empty',
  props: {
    error: {
      type: Object,
      default: null
    }
  },
  head() {
    const title =
      this.error.statusCode === 404 ? this.pageNotFound : this.otherError
    return {
      title
    }
  },
  data() {
    return {
      pageNotFound: '404 Not Found',
      otherError: 'An error occurred'
    }
  }
}
</script>

<style scoped>
h1 {
  font-size: 20px;
}
</style>

pages/posts/add.Vue

<template>
  <v-container>
    <h1>Add Post</h1>
  </v-container>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class AddPostPage extends Vue {
  name: string = 'AddPost'
}
</script>

pages/posts/index.Vue

<template>
  <v-container>
    <h1>Posts</h1>
  </v-container>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
  name: string = 'Posts'
}
</script>

pages/profile/index.Vue

<template>
  <v-container>
    <h1>Posts</h1>
  </v-container>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
  name: string = 'Posts'
}
</script>

pages/signin/index.Vue

<template>
  <v-container>
    <h1>Posts</h1>
  </v-container>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
  name: string = 'Posts'
}
</script>

pages/signup/index.Vue

<template>
  <v-container>
    <h1>Posts</h1>
  </v-container>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
  name: string = 'Posts'
}
</script>

pages/index.Vue

<template>
  <v-container>
    <h1>Home</h1>
  </v-container>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class HomePage extends Vue {
  name: string = 'Home'
}
</script>

6.-Run the application to ensure it's working

  • Browse to http://localhost:3000/

  • It doesn't work. It seems as if the layout/default.vue is not loaded.

7.-Create the typescript branch to keep the changes

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ git checkout -b typescript
Switched to a new branch 'typescript'
M       client/.eslintrc.js
M       client/layouts/default.vue
D       client/nuxt.config.js
M       client/package-lock.json
M       client/package.json
M       client/pages/index.vue
M       client/pages/posts/add.vue
M       client/pages/posts/index.vue
M       client/pages/profile/index.vue
M       client/pages/signin/index.vue
M       client/pages/signup/index.vue
M       package-lock.json
M       package.json
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git add .
warning: LF will be replaced by CRLF in client/.eslintrc.js.
The file will have its original line endings in your working directory
.
.
.

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git status
On branch typescript
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   client/.eslintrc.js
        modified:   client/layouts/default.vue
        renamed:    client/nuxt.config.js -> client/nuxt.config.ts
        modified:   client/package-lock.json
        modified:   client/package.json
        modified:   client/pages/index.vue
        modified:   client/pages/posts/add.vue
        modified:   client/pages/posts/index.vue
        modified:   client/pages/profile/index.vue
        modified:   client/pages/signin/index.vue
        modified:   client/pages/signup/index.vue
        new file:   client/tsconfig.json
        modified:   package-lock.json
        modified:   package.json
  • Push the changes
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git push --set-upstream origin typescript
Enumerating objects: 42, done.
Counting objects: 100% (42/42), done.
Delta compression using up to 4 threads
Compressing objects: 100% (20/20), done.
Writing objects: 100% (23/23), 12.30 KiB | 286.00 KiB/s, done.
Total 23 (delta 12), reused 0 (delta 0)
remote: Resolving deltas: 100% (12/12), completed with 6 local objects.
remote:
remote: Create a pull request for 'typescript' on GitHub by visiting:
remote:      https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/pull/new/typescript
remote:
To https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt.git
 * [new branch]      typescript -> typescript
Branch 'typescript' set up to track remote branch 'typescript' from 'origin'.
  • Go back to the master branch
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

VI.-Set up multi-language

1.-Base the multi-language on the A simple multi-language site with Nuxt.js and nuxt-i18n articule

2.-Install the nuxt-i18n package

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i nuxt-i18n --save
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ nuxt-i18n@5.12.7
added 7 packages from 6 contributors and audited 880983 packages in 37.288s
found 0 vulnerabilities
  • Fix the warn detected
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i vuetify-loader
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ vuetify-loader@1.2.2
added 1 package and audited 880989 packages in 44.82s
found 0 vulnerabilities

3.-Create the lang folder and the translations documents

  • We need to create the lang folder and the es-ES.js for the Spanish translations and the en-US.js for the English translations.

client/lang/es-ES.js

export default {
  posts: 'Entradas',
  signin: 'Iniciar sesión',
  signup: 'Inscripción',
  searchposts: 'Buscar entradas',
  home: 'Inicio'
}

client/lang/en-US.js

export default {
  posts: 'Posts',
  signin: 'Sign in',
  signup: 'Sign up',
  searchposts: 'Search Posts',
  home: 'Home'
}

4.-Modify the nuxt.config.js document to set up the pluging and the initial languages

nuxt.config.js

import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js modules
   */
  modules: [
    [
      'nuxt-i18n',
      {
        seo: false,
        locales: [
          {
            name: 'ESP',
            code: 'es',
            iso: 'es-ES'
          },
          {
            name: 'ENG',
            code: 'en',
            iso: 'en-US'
          }
        ],
        strategy: 'prefix_and_default',
        langDir: 'lang/',
        defaultLocale: 'es',
        vueI18n: {
          fallbackLocale: 'es',
          messages: { es, en }
        }
      }
    ],
    '@nuxtjs/vuetify',
    '@nuxtjs/pwa',
    '@nuxtjs/eslint-module'
  ],
  /*
   ** vuetify module configuration
   ** https://github.com/nuxt-community/vuetify-module
   */
  vuetify: {
    theme: {
      // primary: colors.blue.darken2,
      // accent: colors.grey.darken3,
      // secondary: colors.amber.darken3,
      // info: colors.teal.lighten1,
      // warning: colors.amber.base,
      // error: colors.deepOrange.accent4,
      // success: colors.green.accent3
      primary: '#3B125F',
      secondary: '#8B5FBF',
      accent: '#BF653F',
      error: '#722530',
      warning: '#A37513',
      info: '#396893',
      success: '#4caf50'
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}

5.-Modify the layout to include a lang-switcher button.

client/layouts/default.vue

<template>
  <v-app style="background: #E3E3EE">
    <!-- Side Navbar -->
    <v-navigation-drawer v-model="sideNav" fixed app temporary>
      <v-toolbar color="accent" dark flat>
        <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
        <nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
          <h1 class="title pl-3">VueShare</h1>
        </nuxt-link>
      </v-toolbar>

      <v-divider></v-divider>

      <!-- Side Navbar Links -->
      <v-list>
        <v-list-tile
          v-for="(item, i) in sideNavItems"
          :key="i"
          :to="localePath(item.link)"
          ripple
        >
          <v-list-tile-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            {{ item.title }}
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>

    <!-- Horizontal Navbar -->
    <v-toolbar fixed color="primary" dark>
      <!-- App Title -->
      <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
          VueShare
        </nuxt-link>
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <!-- Search Input -->
      <v-text-field
        flex
        prepend-icon="search"
        color="accent"
        single-line
        hide-details
        :placeholder="$t('searchposts')"
      ></v-text-field>

      <v-spacer></v-spacer>

      <!-- Horizontal Navbar Links -->
      <v-toolbar-items class="hidden-xs-only">
        <v-btn
          v-for="(item, i) in horizontalNavItems"
          :key="i"
          :to="localePath(item.link)"
          flat
        >
          <v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
          {{ item.title }}
        </v-btn>
      </v-toolbar-items>
      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link
          v-for="(locale, i) in showLocales"
          :key="i"
          tag="span"
          style="cursor: pointer"
          class="lang-switcher"
          :to="switchLocalePath(locale.code)"
        >
          {{ locale.name }}
        </nuxt-link>
      </v-toolbar-title>
    </v-toolbar>

    <v-content>
      <v-container class="mt-4">
        <transition name="fade">
          <nuxt />
        </transition>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
export default {
  head() {
    return this.$nuxtI18nSeo()
  },
  data() {
    return {
      sideNav: false
    }
  },
  computed: {
    horizontalNavItems() {
      return [
        { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
        { icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
        { icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
      ]
    },
    sideNavItems() {
      return [
        { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
        { icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
        { icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
      ]
    },
    showLocales() {
      return this.$i18n.locales.filter(
        locale => locale.code !== this.$i18n.locale
      )
    }
  },
  methods: {
    toggleSideNav() {
      this.sideNav = !this.sideNav
    }
  }
}
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition-property: opacity;
  transition-duration: 0.25s;
}

.fade-enter-active {
  transition-delay: 0.25s;
}

.fade-enter,
.fade-leave-active {
  opacity: 0;
}
</style>

6.-Test if the multi-language works with the changes applied

Section 6: Using Vue Apollo 0 / 4|24min

I.-Setting up Apollo Client / Vue Apollo, Firing getPosts Query from Client

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i @nuxtjs/apollo graphql-tag

> core-js@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\apollo-env\node_modules\core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
> https://opencollective.com/core-js 
> https://www.patreon.com/zloirock 

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> protobufjs@6.8.8 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\protobufjs
> node scripts/postinstall

npm WARN vue-cli-plugin-apollo@0.20.0 requires a peer of @vue/cli-shared-utils@^3.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN ts-node@8.3.0 requires a peer of typescript@>=2.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ graphql-tag@2.10.1
+ @nuxtjs/apollo@4.0.0-rc7
added 100 packages from 126 contributors and audited 883960 packages in 64.792s
found 0 vulnerabilities
  • Create the apollo folder and the apollo/customErrorHandler.js document.

apollo/customErrorHandler.js

export default (err, { error }) => {
  console.log(err)
  error({ statusCode: 304, message: 'Server error!' })
}
  • Create the gql folder and the gql/getPosts.gql document.

gql/getPosts.gql

query {
  getPosts {
    _id
    title
    imageUrl
    description
    likes
  }
}
  • We are going to modify the client/nuxt.config.js document to set up the use of the @nuxtjs/apollo package.

client/nuxt.config.js

import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js modules
   */
  modules: [
    [
      'nuxt-i18n',
      {
        seo: false,
        locales: [
          {
            name: 'ESP',
            code: 'es',
            iso: 'es-ES'
          },
          {
            name: 'ENG',
            code: 'en',
            iso: 'en-US'
          }
        ],
        strategy: 'prefix_and_default',
        langDir: 'lang/',
        defaultLocale: 'es',
        vueI18n: {
          fallbackLocale: 'es',
          messages: { es, en }
        }
      }
    ],
    '@nuxtjs/vuetify',
    '@nuxtjs/pwa',
    '@nuxtjs/eslint-module',
    '@nuxtjs/apollo'
  ],
  apollo: {
    errorHandler: '~/apollo/customErrorHandler.js',
    clientConfigs: {
      default: {
        httpEndpoint:
          process.env.HTTP_ENDPOINT || 'http://localhost:4000/graphql'
      }
    }
  },
  /*
   ** vuetify module configuration
   ** https://github.com/nuxt-community/vuetify-module
   */
  vuetify: {
    theme: {
      // primary: colors.blue.darken2,
      // accent: colors.grey.darken3,
      // secondary: colors.amber.darken3,
      // info: colors.teal.lighten1,
      // warning: colors.amber.base,
      // error: colors.deepOrange.accent4,
      // success: colors.green.accent3
      primary: '#3B125F',
      secondary: '#8B5FBF',
      accent: '#BF653F',
      error: '#722530',
      warning: '#A37513',
      info: '#396893',
      success: '#4caf50'
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}

  • We are going to modify the client/pages/index.vue document to include a call to the getPosts graphql query from the server.

client/pages/index.vue

<template>
  <v-container>
    <h1>Home</h1>
    <ul v-for="post in getPosts" :key="post._id">
      <li>{{ post.title }} {{ post.imageUrl }} {{ post.description }}</li>
      <li>{{ post.likes }}</li>
    </ul>
  </v-container>
</template>

<script>
import { getPosts } from '~/gql/getPosts.gql'
export default {
  name: 'Home',
  apollo: {
    getPosts: {
      query: getPosts
    }
  }
}
</script>


  • We need top test if everything works correctly.

  • We are going to modify the client/pages/index.vue document to include the Corousel Vuetify components.

client/pages/index.vue

<template>
  <v-container v-if="getPosts" text-xs-center>
    <v-flex xs12>
      <v-carousel v-bind="{ cycle: true }" interval="3000">
        <v-carousel-item
          v-for="post in getPosts"
          :key="post._id"
          :src="post.imageUrl"
        >
          <h1 id="carousel__title">{{ post.title }}</h1>
        </v-carousel-item>
      </v-carousel>
    </v-flex>
  </v-container>
</template>

<script>
import { getPosts } from '~/gql/getPosts.gql'
export default {
  name: 'Home',
  apollo: {
    getPosts: {
      query: getPosts
    }
  }
}
</script>

<style>
#carousel__title {
  position: absolute;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  border-radius: 5px 5px 0 0;
  padding: 0.5em;
  margin: 0 auto;
  bottom: 50px;
  left: 0;
  right: 0;
}
</style>

Section 7: Integrate Vuex with ApolloClient

I.Create the store to manage the GraphQL getPosts query

client\store\index.js

import { getPosts } from '~/gql/getPosts.gql'

export const state = () => ({
  posts: [],
  loading: false
})
export const mutations = {
  setPosts: (state, payload) => {
    state.posts = payload
  },
  setLoading: (state, payload) => {
    state.loading = payload
  }
}
export const actions = {
  async nuxtServerInit({ dispatch }) {
    await dispatch('getPosts')
  },
  async getPosts({ commit }) {
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.query({
        query: getPosts
      })
      commit('setPosts', result.data.getPosts)
    } catch (error) {
      console.log(error)
    }
    commit('setLoading', false)
  }
}
export const getters = {
  posts: state => state.posts,
  loading: state => state.loading
}

II.-Modify the Home page to read the Posts from the store

client\pages\index.vue

<template>
  <v-container text-xs-center>
    <v-layout row>
      <v-dialog v-model="loading" persistent fullscreen>
        <v-container fill-height>
          <v-layout row justify-center align-center>
            <v-progress-circular
              indeterminate
              :size="70"
              :width="7"
              color="secondary"
            ></v-progress-circular>
          </v-layout>
        </v-container>
      </v-dialog>
    </v-layout>

    <v-flex xs12>
      <v-carousel
        v-if="!loading && posts.length > 0"
        v-bind="{ cycle: true }"
        interval="3000"
      >
        <v-carousel-item
          v-for="post in posts"
          :key="post._id"
          :src="post.imageUrl"
        >
          <h1 id="carousel__title">{{ post.title }}</h1>
        </v-carousel-item>
      </v-carousel>
    </v-flex>
  </v-container>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Home',
  computed: {
    ...mapGetters(['loading', 'posts'])
  }
}
</script>

<style>
#carousel__title {
  position: absolute;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  border-radius: 5px 5px 0 0;
  padding: 0.5em;
  margin: 0 auto;
  bottom: 50px;
  left: 0;
  right: 0;
}
</style>

III.-Test the integration of Vuex with ApolloClient

Section 8: JWT Authentication for Signin / Signup

I.-Create Gravatar Avatar and Hash User Passwords on Signup

  • We need to install the MD5 package, a JavaScript function for hashing messages with MD5, and the node.bcrypt.js package, A library to help you hash passwords.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm i md5 bcrypt

> bcrypt@3.0.6 install C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\node_modules\bcrypt
> node-pre-gyp install --fallback-to-build

node-pre-gyp WARN Using needle for node-pre-gyp https download
[bcrypt] Success: "C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\node_modules\bcrypt\lib\binding\bcrypt_lib.node" is installed via remote
npm WARN full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 scripts['server'] should probably be scripts['start'].

+ md5@2.2.1
+ bcrypt@3.0.6
added 65 packages from 51 contributors and audited 202 packages in 8.927s
found 0 vulnerabilities
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm --save-devnpm i --save-dev @types/md5 @types/bcrypt
npm WARN full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 scripts['server'] should probably be scripts['start'].

+ @types/md5@2.1.33
+ @types/bcrypt@3.0.0
added 3 packages from 41 contributors and audited 205 packages in 5.411s
found 0 vulnerabilities
  • We need to modify the User's Model to put the @prop attribute to the avatar field.

server\src\users\model\user.model.ts

import * as mongoose from "mongoose";
import { prop, Typegoose } from 'typegoose';
import { IsString, IsDate, IsArray } from 'class-validator';

export class User extends Typegoose {
  @IsString()
  @prop({ required: true, unique: true, trim: true })
  username: string;

  @IsString()
  @prop({ required: true, trim: true })
  email: string;

  @IsString()
  @prop({ required: true, trim: true })
  password: string;

  @IsString()
  @prop()
  avatar?: string;

  @IsDate()
  @prop({ default: Date.now })
  joinDate: Date

  @IsArray()
  @prop({ required: true, ref: "Post" })
  favorites: [mongoose.Schema.Types.ObjectId]
}

  • We need to modify the User Service to assign a ramndom avatar to the user and to store the password hashed.

server\src\users\users.service.ts

import * as mongoose from "mongoose";
import { prop, Typegoose } from 'typegoose';
import { IsString, IsDate, IsArray } from 'class-validator';

export class User extends Typegoose {
  @IsString()
  @prop({ required: true, unique: true, trim: true })
  username: string;

  @IsString()
  @prop({ required: true, trim: true })
  email: string;

  @IsString()
  @prop({ required: true, trim: true })
  password: string;

  @IsString()
  @prop()
  avatar?: string;

  @IsDate()
  @prop({ default: Date.now })
  joinDate: Date

  @IsArray()
  @prop({ required: true, ref: "Post" })
  favorites: [mongoose.Schema.Types.ObjectId]
}

  • We can test if the request to sign up a new user is working properly now.
mutation {
  signupUserWithInput(
    input: { username: "John", email: "john@google.com", password: "John" }
  ) {
    _id
    username
    email
    password
    avatar
    joinDate
    favorites
  }
}
{
  "data": {
    "signupUserWithInput": {
      "_id": "5d36a9e007bced421c3a2031",
      "username": "John",
      "email": "john@google.com",
      "password": "$2b$10$OPv9/YQC1zZatdGEDw5YYOjiUE2omaBjuiSn5Bf9cJXahre7A/hNO",
      "avatar": "http://gravatar.com/avatar/61409aa1fd47d4a5332de23cbf59a36f?d=identicon",
      "joinDate": "2019-07-23T06:32:00.328Z",
      "favorites": null
    }
  }
}

II.-Modify the server app to include the authentication.

1.-Install the passport and jwt packaged needed to manage the authentication

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i @nestjs/jwt @nestjs/passport passport passport-jwt
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ passport-jwt@4.0.0
+ passport@0.4.0
+ @nestjs/passport@6.1.0
+ @nestjs/jwt@6.1.1
added 20 packages from 17 contributors and audited 877489 packages in 75.148s
found 0 vulnerabilities
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i --save-dev @types/passport-jwt
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ @types/passport-jwt@3.0.1
added 3 packages from 12 contributors and audited 877533 packages in 15.893s
found 0 vulnerabilities

2.-Create the SECRET environment variable

  • We need to create the new SECRET environment variable needed for the JWT token:

server\fake.env

MONGO_URI=mongodb+srv://USERNAME:PASSWORD@CLUSTERID.mongodb.net/DATABASENAME?retryWrites=true&w=majority
SECRET=mysupersecretencryptionkey
  • We need to modify the config.service.ts document to include the new SECRET environment variable:

server\src\config\config.service.ts

import * as dotenv from 'dotenv'
import * as fs from 'fs'

export class ConfigService {
  MONGODB_URI: string
  private readonly envConfig: { [key: string]: string }

  constructor() {
    if (
      process.env.NODE_ENV === 'production' ||
      process.env.NODE_ENV === 'staging'
    ) {
      this.envConfig = {
        MONGO_URI: process.env.MONGO_URI,
        SECRET: process.env.SECRET
      }
    } else {
      this.envConfig = dotenv.parse(fs.readFileSync('.env'))
    }
  }

  get(key: string): string {
    return this.envConfig[key]
  }
}

3.-Create the Auth Service

  • We need to create the Auth folder to put inside the Auth Service and everything related. This service is going to be used to manage the Authentication and the Authorisation of the GraphQL Queries adn Mutations.

  • We are going to create the jwt-payload.interface.ts to define the content of the 'jwt token`

server\src\auth\jwt-payload.interface.ts

export interface JwtPayload {
  username: string
  email: string
}
  • We need to create the graphql-auth.guard.ts to create the GraphqlAuthGuard guard derived from the NestJS AuthGuard Guard to transform from the Exceution Context to a GraphQL context

server\src\auth\graphql-auth.guard.ts

import { Injectable, ExecutionContext } from "@nestjs/common";
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from "@nestjs/graphql";

@Injectable()
export class GraphqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context)
    return ctx.getContext().req
  }
}
  • We need to create the jwt.strategy.ts document with the JwtStrategy class used to read the User information obtained from the token passed through the authorization header with any request. It is attached to the request on the user object.

server\src\auth\jwt.strategy.ts

import { PassportStrategy } from '@nestjs/passport'
import { Strategy, ExtractJwt } from 'passport-jwt'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtPayload } from './jwt-payload.interface'
import { InjectModel } from 'nestjs-typegoose'
import { User } from '../users/model/user.model'
import { ModelType } from 'typegoose'
import { ConfigService } from '../config/config.service'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectModel(User) private readonly userModel: ModelType<User>,
    private readonly configService: ConfigService
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromHeader("authorization"),
      secretOrKey: configService.get('SECRET'),
    })
  }

  async validate(payload: JwtPayload): Promise<User> {
    const { username } = payload
    const user = await this.userModel.findOne({
      username: username
    }).populate({
      path: "favorites",
      model: "Post"
    })
    if (!user) {
      throw new UnauthorizedException()
    }
    return user
  }
}
  • We are going to create the current-user.decorator.ts to create the CurrentUser custom decorator with User information obtained from the token passed through the authentication header with any request. Please, note it is authentication instead of authorisation because it is the standard header for Apollo.

server\src\auth\current-user.decorator.ts

import { createParamDecorator } from '@nestjs/common'

export const CurrentUser = createParamDecorator(
  (_data, [,, ctx]) => ctx.req.user,
)
  • We are going to create the auth.service.ts document with the AuthService service that has the signUp and signIn methods.

server\src\auth\auth.service.ts

import { Injectable, ConflictException, InternalServerErrorException, UnauthorizedException } from '@nestjs/common'
import { InjectModel } from 'nestjs-typegoose'
import { ModelType } from 'typegoose'
import { JwtService } from '@nestjs/jwt'
import { SigninUserDto } from '../users/model/dtos/signin-user.dto'
import { CreateUserDto } from '../users/model/dtos/create-user.dto'
import { Token, User } from '../users/model/user.model'
import { JwtPayload } from './jwt-payload.interface'
import md5 from 'md5'
import * as bcrypt from 'bcrypt'

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(User) private readonly userModel: ModelType<User>,
    private readonly jwtService: JwtService,
  ) { }

  async signUp(createUserDto: CreateUserDto): Promise<Token> {
    const username = createUserDto.username
    const user = await this.userModel.findOne({ username })
    if (user) {
      throw new ConflictException("User already exists")
    }
    const avatar = `http://gravatar.com/avatar/${md5(username)}?d=identicon`
    var salt = bcrypt.genSaltSync(10)
    var password = bcrypt.hashSync(createUserDto.password, salt)
    const currentUser = { ...createUserDto, avatar, password }
    const createdUser = new this.userModel(currentUser)
    try {
      const newUser = await createdUser.save()
      const token = this.getAccessToken(newUser)
      return token
    }
    catch (error) {
      throw new InternalServerErrorException()
    }

  }

  async signIn(signinUserDto: SigninUserDto): Promise<Token> {
    const { username, password } = signinUserDto
    const user = await this.userModel.findOne({ username })
    if (!user) {
      throw new UnauthorizedException("Invalid credentials")
    }
    const isValidPassword = await bcrypt.compare(password, user.password)
    if (!isValidPassword) {
      throw new UnauthorizedException("Invalid credentials")
    }
    const token = this.getAccessToken(user)
    return token
  }

  private async getAccessToken(user: User): Promise<Token> {
    const { username, email } = user
    const payload: JwtPayload = { username, email }
    const token = await this.jwtService.sign(payload)
    return { token }
  }
}
  • We finally need to create the auth.module.ts with the AuthModule module. It needs to import the PassportModule and the JwtModule.

server\src\auth\auth.module.ts

import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
import { JwtStrategy } from './jwt.strategy'
import { ConfigModule } from '../config/config.module';
import { ConfigService } from '../config/config.service';
import { TypegooseModule } from 'nestjs-typegoose'
import { User } from '../users/model/user.model';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get('SECRET'),
        signOptions: {
            expiresIn: 216000, // 1 Hour: 60 * 60 * 60
        },
      }),
    }),
    TypegooseModule.forFeature([User]),    

  ],
  providers: [
    JwtStrategy,
    AuthService,
    ConfigService
  ],
  exports: [
    AuthService
  ],
})
export class AuthModule {}
  • We need to modify the app.module.ts document to include the context as part of the GraphQLModule module.

server\src\app.module.ts

import { Module } from "@nestjs/common"
import { AppService } from "./app.service"
import { DatabaseModule } from "./database/database.module"
import { GraphQLModule } from "@nestjs/graphql"
import { UsersModule } from './users/users.module'
import { PostsModule } from './posts/posts.module'

@Module({
  imports: [
    DatabaseModule,
    GraphQLModule.forRoot({
      autoSchemaFile: "schema.gql",
      context: ({ req }) => ({ req }),
    }),
    UsersModule,
    PostsModule,
  ],
  providers: [AppService]
})
export class AppModule { }
  • We need to modify the server\src\users\users.module.ts document to inlcude the ConfigService provider adn the AuthModule module.

server\src\users\users.module.ts

import { Module } from '@nestjs/common'
import { TypegooseModule } from 'nestjs-typegoose'
import { User } from './model/user.model'
import { UsersService } from './users.service'
import { UsersResolver } from "./graphql/users.resolver"
import { ConfigService } from '../config/config.service'
import { AuthModule } from '../auth/auth.module'

@Module({
  imports: [
    TypegooseModule.forFeature([User]),
    AuthModule
  ],
  providers: [UsersService, UsersResolver, ConfigService]
})
export class UsersModule { }

  • We are going to create the new SignUserDto Data Transfer Object.

server\src\users\model\dtos\signin-user.dto.ts

export class SigninUserDto {
  readonly username: string;
  readonly password: string
}
  • We are going to modify the user.type.ts and user.model.ts documents to include the Token class.

server\src\users\graphql\types\user.type.ts

import { Field, ObjectType, ID } from 'type-graphql'

@ObjectType()
export class Token {
  @Field()
  readonly token: string  
}

@ObjectType()
export class User {
  @Field(() => ID)
  readonly _id: string
  @Field()
  readonly username: string
  @Field()
  readonly email: string
  @Field()
  readonly password: string
  @Field({ nullable: true })
  readonly avatar: string
  @Field()
  readonly joinDate: Date
  @Field(() => [ID], { nullable: true })
  readonly favorites: string[]
}

server\src\users\model\user.model.ts

import * as mongoose from "mongoose"
import { prop, Typegoose } from 'typegoose'
import { IsString, IsDate, IsArray } from 'class-validator'


export class Token {
  @IsString()
  readonly token: string  
}

export class User extends Typegoose {
  @IsString()
  @prop({ required: true, unique: true, trim: true })
  username: string

  @IsString()
  @prop({ required: true, trim: true })
  email: string

  @IsString()
  @prop({ required: true, trim: true })
  password: string

  @IsString()
  @prop()
  avatar?: string

  @IsDate()
  @prop({ default: Date.now })
  joinDate: Date

  @IsArray()
  @prop({ required: true, ref: "Post" })
  favorites: [mongoose.Schema.Types.ObjectId]
}


  • We are going to modify the users.service.ts document to remove the signupUser and createToken methods that are now part of the AuthService.

server\src\users\users.service.ts

import { Injectable } from '@nestjs/common'
import { InjectModel } from 'nestjs-typegoose'
import { User } from './model/user.model'
import { ModelType } from 'typegoose'

@Injectable()
export class UsersService {
  constructor(@InjectModel(User) private readonly userModel: ModelType<User>) { }

  async getUsers(): Promise<User[] | null> {
    return await this.userModel.find().exec()
  }

}

  • We are going to modify the users.resolver.ts document to include the AuthService service and to include the new signinUser mutation, the new getCurrentUser query and to modify the signupUser mutation.

server\src\users\graphql\users.resolver.ts

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'
import { UsersService } from '../users.service'
import { User, Token } from './types/user.type'
import { GraphqlAuthGuard } from '../../auth/graphql-auth.guard'
import { CurrentUser } from '../../auth/current-user.decorator'
import { AuthService } from '../../auth/auth.service'
import { UseGuards } from '@nestjs/common'

@Resolver()
export class UsersResolver {
  constructor(
    private readonly usersService: UsersService,
    private readonly authService: AuthService,
  ) { }

  @Query(() => User)
  @UseGuards(GraphqlAuthGuard)
  async getCurrentUser(@CurrentUser() currentUser: User) {
    return currentUser
  }

  @Query(() => [User])
  async getUsers() {
    return await this.usersService.getUsers()
  }

  @Mutation(() => Token)
  async signupUser(
    @Args('username') username: string,
    @Args('email') email: string,
    @Args('password') password: string,
  ) {
    return await this.authService.signUp({ username, email, password })
  }

  @Mutation(() => Token)
  async signinUser(
    @Args('username') username: string,
    @Args('password') password: string,
  ) {
    return await this.authService.signIn({ username,  password })
  }
}
  • We are going to check if the new signinUser mutation works properly:

Request

mutation {
  signinUser(username: "Fofo", password: "Fofo") {
    token
  }
}

Response

{
  "data": {
    "signinUser": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkZvZm8iLCJlbWFpbCI6IkZvZm9AZ21haWwuY29tIiwiaWF0IjoxNTY1MDIxMTQ5LCJleHAiOjE1NjUyMzcxNDl9.OtGTm2qHC5XT-hqNmb6eypFHrZg3DRwBj7ozac3CPCs"
    }
  }
}
  • We are going to check if the signupUser mutation still works properly, but returning a Token instead of the user:

Request

mutation {
  signupUser(username: "David", email: "david@gmail.com", password: "David") {
		token
  }
}

Response

{
  "data": {
    "signupUser": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkRhdmlkIiwiZW1haWwiOiJkYXZpZEBnbWFpbC5jb20iLCJpYXQiOjE1NjUwMTc4MzksImV4cCI6MTU2NTIzMzgzOX0.KcnzxLj4Wu9RkjT9KD6L55ILzBzYk0NNdnWp6o1-nFk"
    }
  }
}
  • We are going to check if the new getCurrentUser query works properly:

Request

query {
    getCurrentUser {
      _id
      username
      email
      password
      avatar
      joinDate
      favorites 
    }
  }

Http header

{
  "authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkZvZm8iLCJlbWFpbCI6IkZvZm9AZ21haWwuY29tIiwiaWF0IjoxNTY1MDIxMTQ5LCJleHAiOjE1NjUyMzcxNDl9.OtGTm2qHC5XT-hqNmb6eypFHrZg3DRwBj7ozac3CPCs"
}

Response

{
  "data": {
    "getCurrentUser": {
      "_id": "5d35390f58729c10f875d41c",
      "username": "Fofo",
      "email": "Fofo@gmail.com",
      "password": "$2b$10$qb.j.FtbHSfSgyH8ZYGGwOW.cIFj8voPEs1m/jcmdFrX/j/kP1zky",
      "avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon",
      "joinDate": "2019-07-22T04:18:23.184Z",
      "favorites": null
    }
  }
}

III.-Modify the client app to include the Authentication.

1.-Modify the Nuxt.Config to set up Apollo Authentication

  • We need to modify the nuxt.config.js document to add the authenticationType: '', property to the apollo tag. Otherwise it would included the Bearer prefix as the default value.

client\nuxt.config.js

import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js modules
   */
  modules: [
    [
      'nuxt-i18n',
      {
        seo: false,
        locales: [
          {
            name: 'ESP',
            code: 'es',
            iso: 'es-ES'
          },
          {
            name: 'ENG',
            code: 'en',
            iso: 'en-US'
          }
        ],
        strategy: 'prefix_and_default',
        langDir: 'lang/',
        defaultLocale: 'es',
        vueI18n: {
          fallbackLocale: 'es',
          messages: { es, en }
        }
      }
    ],
    '@nuxtjs/vuetify',
    '@nuxtjs/pwa',
    '@nuxtjs/eslint-module',
    '@nuxtjs/apollo'
  ],
  apollo: {
    errorHandler: '~/apollo/customErrorHandler.js',
    authenticationType: '',
    clientConfigs: {
      default: {
        httpEndpoint:
          process.env.HTTP_ENDPOINT || 'http://localhost:4000/graphql'
      }
    }
  },
  /*
   ** vuetify module configuration
   ** https://github.com/nuxt-community/vuetify-module
   */
  vuetify: {
    theme: {
      // primary: colors.blue.darken2,
      // accent: colors.grey.darken3,
      // secondary: colors.amber.darken3,
      // info: colors.teal.lighten1,
      // warning: colors.amber.base,
      // error: colors.deepOrange.accent4,
      // success: colors.green.accent3
      primary: '#3B125F',
      secondary: '#8B5FBF',
      accent: '#BF653F',
      error: '#722530',
      warning: '#A37513',
      info: '#396893',
      success: '#4caf50'
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}

2.-Create the gql documents with queries and mutations

client\gql\getCurrentUser.gql

query getCurrentUser {
  getCurrentUser {
    _id
    username
    email
    password
    avatar
    joinDate
    favorites {
      _id
      title
      imageUrl
    }
  }
}

client\gql\signinUser.gql

  mutation signinUser($username: String!, $password: String!) {
    signinUser(username: $username, password: $password) {
      token
    }
  }

client\gql\signupUser.gql

  mutation signupUser($username: String!, $email: String!, $password: String!) {
    signupUser(username: $username, email: $email, password: $password) {
      token
    }
  }

3.-Update the localization documents with new entries

client\lang\en-US.js

export default {
  posts: 'Posts',
  signin: 'Sign in',
  signup: 'Sign up',
  searchposts: 'Search Posts',
  home: 'Home',
  createPost: 'Create Post',
  profile: 'Profile',
  signout: 'Signout',
  password: 'Password',
  username: 'Username',
  dontHaveAnAccount: "Don't have an account?",
  welcomeBack: 'Welcome Back!'
}

client\lang\es-ES.js

export default {
  posts: 'Entradas',
  signin: 'Iniciar sesión',
  signup: 'Inscripción',
  searchposts: 'Buscar entradas',
  home: 'Inicio',
  createPost: 'Crear Entrada',
  profile: 'Perfil',
  signout: 'Cerrar sesión',
  username: 'Usuario',
  password: 'Contraseña',
  dontHaveAnAccount: '¿No tiene una cuenta?',
  welcomeBack: '¡Bienvenido de nuevo!'
}

4.-Update the store to manaage the Authentication

client\store\index.js

import { getPosts } from '~/gql/getPosts.gql'
import { getCurrentUser } from '~/gql/getCurrentUser.gql'
import { signinUser } from '~/gql/signinUser.gql'

export const state = () => ({
  posts: [],
  user: null,
  loading: false
})
export const mutations = {
  setPosts: (state, payload) => {
    state.posts = payload
  },
  setUser: (state, payload) => {
    state.user = payload
  },
  setLoading: (state, payload) => {
    state.loading = payload
  },
  clearUser: state => (state.user = null)
}
export const actions = {
  async nuxtServerInit({ dispatch }) {
    await dispatch('getCurrentUser')
    await dispatch('getPosts')
  },
  async getPosts({ commit }) {
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.query({
        query: getPosts
      })
      commit('setPosts', result.data.getPosts)
    } catch (error) {
      console.log(error)
    }
    commit('setLoading', false)
  },
  async getCurrentUser({ commit }) {
    if (!this.app.$apolloHelpers.getToken()) {
      commit('clearUser')
      return
    }
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.query({
        query: getCurrentUser
      })
      // Add user data to state
      commit('setUser', result.data.getCurrentUser)
    } catch (error) {
      console.error(error)
    }
    commit('setLoading', false)
  },
  async signinUser({ commit, dispatch }, payload) {
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.mutate({
        mutation: signinUser,
        variables: payload
      })
      await this.app.$apolloHelpers.onLogin(result.data.signinUser.token)
      await dispatch('getCurrentUser')
      commit('setLoading', false)
    } catch (error) {
      console.error(error)
      commit('setLoading', false)
    }
  },
  async signoutUser({ commit }) {
    // clear user in state
    commit('clearUser')
    await this.app.$apolloHelpers.onLogout()
    // redirect home - kick users out of private pages (i.e. profile)
    this.app.router.push(this.app.localePath('index'))
  }
}
export const getters = {
  posts: state => state.posts,
  user: state => state.user,
  loading: state => state.loading
}

5.-Update the SignIn page

client\pages\signin\index.vue

<template>
  <v-container text-xs-center mt-5 pt-5>
    <!-- Signin Title -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <h1>{{ $t('welcomeBack') }}</h1>
      </v-flex>
    </v-layout>

    <!-- Signin Form -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <v-card color="secondary" dark>
          <v-container>
            <v-form @submit.prevent="handleSigninUser">
              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="username"
                    prepend-icon="face"
                    :label="$t('username')"
                    type="text"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="password"
                    prepend-icon="extension"
                    :label="$t('password')"
                    type="password"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-btn color="accent" type="submit">{{ $t('signin') }}</v-btn>
                  <h3>
                    {{ $t('dontHaveAnAccount') }}
                    <nuxt-link to="/signup">{{ $t('signup') }}</nuxt-link>
                  </h3>
                </v-flex>
              </v-layout>
            </v-form>
          </v-container>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Signin',
  data() {
    return {
      username: '',
      password: ''
    }
  },
  computed: {
    ...mapGetters(['user'])
  },
  watch: {
    user(value) {
      // if user value changes, redirect to home page
      if (value) {
        this.$router.push(this.localePath('index'))
      }
    }
  },
  methods: {
    handleSigninUser() {
      this.$store.dispatch('signinUser', {
        username: this.username,
        password: this.password
      })
    }
  }
}
</script>

6.-Create the auth middleware

client\middleware\auth.js

export default function({ store, redirect, app }) {
  if (!store.getters.user) {
    return redirect(app.localePath('signin'))
  }
}

7.-Update the Profile page to use the auth middleware

client\pages\profile\index.vue

<template>
  <v-container>
    <h1>Profile</h1>
  </v-container>
</template>

<script>
export default {
  name: 'Profile',
  middleware: 'auth'
}
</script>

8.-Update the Layout with new options

client\layouts\default.vue

<template>
  <v-app style="background: #E3E3EE">
    <!-- Side Navbar -->
    <v-navigation-drawer v-model="sideNav" fixed app temporary>
      <v-toolbar color="accent" dark flat>
        <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
        <nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
          <h1 class="title pl-3">VueShare</h1>
        </nuxt-link>
      </v-toolbar>

      <v-divider></v-divider>

      <!-- Side Navbar Links -->
      <v-list>
        <v-list-tile
          v-for="(item, i) in sideNavItems"
          :key="i"
          :to="localePath(item.link)"
          ripple
        >
          <v-list-tile-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            {{ item.title }}
          </v-list-tile-content>
        </v-list-tile>

        <!-- Signout Button -->
        <v-list-tile v-if="user" @click="handleSignoutUser">
          <v-list-tile-action>
            <v-icon>exit_to_app</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>{{ $t('signout') }}</v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>

    <!-- Horizontal Navbar -->
    <v-toolbar fixed color="primary" dark>
      <!-- App Title -->
      <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
          VueShare
        </nuxt-link>
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <!-- Search Input -->
      <v-text-field
        flex
        prepend-icon="search"
        color="accent"
        single-line
        hide-details
        :placeholder="$t('searchposts')"
      ></v-text-field>

      <v-spacer></v-spacer>

      <!-- Horizontal Navbar Links -->
      <v-toolbar-items class="hidden-xs-only">
        <v-btn
          v-for="(item, i) in horizontalNavItems"
          :key="i"
          :to="localePath(item.link)"
          flat
        >
          <v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
          {{ item.title }}
        </v-btn>
      </v-toolbar-items>

      <!-- Profile Button -->
      <v-btn v-if="user" flat to="profile">
        <v-icon class="hidden-sm-only" left>account_box</v-icon>
        <v-badge right color="blue darken-2">
          <!-- <span slot="badge"></span> -->
          {{ $t('profile') }}
        </v-badge>
      </v-btn>

      <!-- Signout Button -->
      <v-btn v-if="user" flat @click="handleSignoutUser">
        <v-icon class="hidden-sm-only" left>exit_to_app</v-icon>
        {{ $t('signout') }}
      </v-btn>

      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link
          v-for="(locale, i) in showLocales"
          :key="i"
          tag="span"
          style="cursor: pointer"
          class="lang-switcher"
          :to="switchLocalePath(locale.code)"
        >
          {{ locale.name }}
        </nuxt-link>
      </v-toolbar-title>
    </v-toolbar>

    <!-- App Content -->
    <main>
      <v-container class="mt-4">
        <transition name="fade">
          <nuxt />
        </transition>
      </v-container>
    </main>
  </v-app>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  head() {
    return this.$nuxtI18nSeo()
  },
  data() {
    return {
      sideNav: false
    }
  },
  computed: {
    ...mapGetters(['user']),
    horizontalNavItems() {
      let items = [
        { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
        { icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
        { icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
      ]
      if (this.user) {
        items = [{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' }]
      }
      return items
    },
    sideNavItems() {
      let items = [
        { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
        { icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
        { icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
      ]
      if (this.user) {
        items = [
          { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
          {
            icon: 'stars',
            title: this.$i18n.t('createPost'),
            link: 'posts-add'
          },
          { icon: 'account_box', title: this.$i18n.t('posts'), link: 'profile' }
        ]
      }
      return items
    },
    showLocales() {
      return this.$i18n.locales.filter(
        locale => locale.code !== this.$i18n.locale
      )
    }
  },
  methods: {
    handleSignoutUser() {
      this.$store.dispatch('signoutUser')
    },
    toggleSideNav() {
      this.sideNav = !this.sideNav
    }
  }
}
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition-property: opacity;
  transition-duration: 0.25s;
}

.fade-enter-active {
  transition-delay: 0.25s;
}

.fade-enter,
.fade-leave-active {
  opacity: 0;
}
</style>

9.-Check if the changes works properly

  • We can try if we can access the profile without being authenticated

  • Check how the cookie is created:

Section 9: Error Handling and Form Validation

I.-Modify the server app to avoid ignoring expiration of the jwt token.

  • We need to modify the jwt.strategy.ts document to add the option ignoreExpiration: false.

server\src\auth\jwt.strategy.ts

import { PassportStrategy } from '@nestjs/passport'
import { Strategy, ExtractJwt } from 'passport-jwt'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtPayload } from './jwt-payload.interface'
import { InjectModel } from 'nestjs-typegoose'
import { User } from '../users/model/user.model'
import { ModelType } from 'typegoose'
import { ConfigService } from '../config/config.service'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectModel(User) private readonly userModel: ModelType<User>,
    private readonly configService: ConfigService
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromHeader("authorization"),
      ignoreExpiration: false,
      secretOrKey: configService.get('SECRET'),
    })
  }

  async validate(payload: any): Promise<User | null> {
    const { username } = payload
    const user = await this.userModel.findOne({
      username: username
    }).populate({
      path: "favorites",
      model: "Post"
    })
    if (!user) {
      throw new UnauthorizedException()
    }
    return user
  }
}

II.-Modify the client app to include Error Handling and Form Validation.

  • We are going to install the cookie-universal-nuxt package to be able to remove cookies on both client and server sides.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i cookie-universal-nuxt
npm WARN ts-node@8.3.0 requires a peer of typescript@>=2.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ cookie-universal-nuxt@2.0.17
added 3 packages from 2 contributors and audited 897031 packages in 67.312s
found 0 vulnerabilities

2.-Create the FormAlert Component and register it globally

  • We need to create the FormAlert.vue Component document. Create the Shared folder from client\components and the FormAlert.vue document in it.

client\components\Shared\FormAlert.vue

<template>
  <v-alert type="error" :value="true" transition="scale-transition" dismissible>
    <h3>{{ message }}</h3>
  </v-alert>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      required: true
    }
  }
}
</script>

  • We need to create the shared-components.js plugin component that will be used to register all the components from the client\components\Shared folder as Global components. It uses the WebPack require-context method.

client\plugins\shared-components.js

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  '~/components/Shared',
  false,
  /[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  const componentConfig = requireComponent(fileName)

  const componentName = upperFirst(
    camelCase(fileName.replace(/^\.\/(.*)\.\w+$/, '$1'))
  )

  Vue.component(componentName, componentConfig.default || componentConfig)
})

3.-Modify the nuxt.config.js document to add a new module and a new pugin

  • We need to modify the nuxt.config.js document to include the new cookie-universal-nuxt module and to register the shared-components plugin.

client\nuxt.config.js

import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: ['@plugins/shared-components.js'],
  /*
   ** Nuxt.js modules
   */
  modules: [
    [
      'nuxt-i18n',
      {
        seo: false,
        locales: [
          {
            name: 'ESP',
            code: 'es',
            iso: 'es-ES'
          },
          {
            name: 'ENG',
            code: 'en',
            iso: 'en-US'
          }
        ],
        strategy: 'prefix_and_default',
        langDir: 'lang/',
        defaultLocale: 'es',
        vueI18n: {
          fallbackLocale: 'es',
          messages: { es, en }
        }
      }
    ],
    '@nuxtjs/vuetify',
    '@nuxtjs/pwa',
    '@nuxtjs/eslint-module',
    '@nuxtjs/apollo',
    `cookie-universal-nuxt`
  ],
  apollo: {
    authenticationType: '',
    errorHandler: '~/apollo/customErrorHandler.js',
    clientConfigs: {
      default: {
        httpEndpoint:
          process.env.HTTP_ENDPOINT || 'http://localhost:4000/graphql'
      }
    }
  },
  /*
   ** vuetify module configuration
   ** https://github.com/nuxt-community/vuetify-module
   */
  vuetify: {
    theme: {
      // primary: colors.blue.darken2,
      // accent: colors.grey.darken3,
      // secondary: colors.amber.darken3,
      // info: colors.teal.lighten1,
      // warning: colors.amber.base,
      // error: colors.deepOrange.accent4,
      // success: colors.green.accent3
      primary: '#3B125F',
      secondary: '#8B5FBF',
      accent: '#BF653F',
      error: '#722530',
      warning: '#A37513',
      info: '#396893',
      success: '#4caf50'
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}

4.-Create a new utils.js document that will contain global functions

  • Create the client\helpers folder and the utils.js document inside with global functions.

client\helpers\utils.js

const utils = {
  isJwtTokenValid(jwtToken) {
    if (!jwtToken) {
      return false
    }
    const items = jwtToken.split('.')
    if (!items) {
      return false
    }
    return items.length > 2
  },
  getFirstGraphQLError(graphQLError) {
    if (
      graphQLError &&
      graphQLError.graphQLErrors &&
      graphQLError.graphQLErrors.length > 0
    ) {
      return graphQLError.graphQLErrors[0]
    }
    if (graphQLError && graphQLError.message) {
      return graphQLError.message
    }
    return graphQLError
  },
  getCurrentGraphQLError(graphQLError) {
    const error = this.getFirstGraphQLError(graphQLError)
    if (!error) {
      return graphQLError
    }
    const currentError =
      error.message && (!error.message.message || !error.message.error)
        ? error.message
        : error.error
        ? error.error
        : error.message && error.message.message
        ? error.message.message
        : error.message && error.message.error
        ? error.message.error
        : error
    return currentError
  }
}

export default utils

5.-Modify the store to include the Signup action and the error and authError states.

  • We need to modify the index.js store document to include the Signup action and the error and authError states.

client\store\index.js

import { getPosts } from '~/gql/getPosts.gql'
import { getCurrentUser } from '~/gql/getCurrentUser.gql'
import { signinUser } from '~/gql/signinUser.gql'
import { signupUser } from '~/gql/signupUser.gql'
import utils from '~/helpers/utils'

export const state = () => ({
  posts: [],
  user: null,
  loading: false,
  error: null,
  authError: null
})
export const mutations = {
  setPosts: (state, payload) => {
    state.posts = payload
  },
  setUser: (state, payload) => {
    state.user = payload
  },
  setLoading: (state, payload) => {
    state.loading = payload
  },
  clearUser: state => (state.user = null),
  setError: (state, payload) => {
    state.error = payload
  },
  clearError: state => (state.error = null),
  setAuthError: (state, payload) => {
    state.authError = payload
  },
  clearAuthError: state => (state.authError = null)
}
export const actions = {
  async nuxtServerInit({ dispatch }) {
    await dispatch('getCurrentUser')
    await dispatch('getPosts')
  },
  async getPosts({ commit }) {
    commit('clearError')
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.query({
        query: getPosts
      })
      commit('setPosts', result.data.getPosts)
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      commit('setError', currentError)
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async logOut({ commit }) {
    this.app.$cookies.remove('apollo-token')
    await this.app.$apolloHelpers.onLogout()
    commit('clearUser')
  },
  async getCurrentUser({ commit, dispatch }) {
    if (!utils.isJwtTokenValid(this.app.$apolloHelpers.getToken())) {
      await dispatch('logOut')
      return
    }
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.query({
        query: getCurrentUser
      })
      // Add user data to state
      commit('setUser', result.data.getCurrentUser)
      commit('clearError')
      commit('clearAuthError')
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      if (currentError === 'Unauthorized') {
        commit('setAuthError', currentError)
        await dispatch('logOut')
      } else {
        commit('setError', currentError)
      }
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async signinUser({ commit, dispatch }, payload) {
    commit('clearError')
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.mutate({
        mutation: signinUser,
        variables: payload
      })
      await this.app.$apolloHelpers.onLogin(result.data.signinUser.token)
      await dispatch('getCurrentUser')
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      commit('setError', currentError)
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async signupUser({ commit, dispatch }, payload) {
    commit('clearError')
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.mutate({
        mutation: signupUser,
        variables: payload
      })
      await this.app.$apolloHelpers.onLogin(result.data.signupUser.token)
      await dispatch('getCurrentUser')
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      commit('setError', currentError)
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async signoutUser({ dispatch }) {
    await dispatch('logOut')
    // redirect home - kick users out of private pages (i.e. profile)
    this.app.router.push(this.app.localePath('index'))
  }
}
export const getters = {
  posts: state => state.posts,
  user: state => state.user,
  loading: state => state.loading,
  error: state => state.error,
  authError: state => state.authError
}

6.-Update the localization documents with new entries

client\lang\en-US.js

export default {
  posts: 'Posts',
  signin: 'Sign in',
  signup: 'Sign up',
  searchposts: 'Search Posts',
  home: 'Home',
  createPost: 'Create Post',
  profile: 'Profile',
  signout: 'Signout',
  username: 'Username',
  password: 'Password',
  confirmPassword: 'Confirm Password',
  email: 'Email',
  dontHaveAnAccount: "Don't have an account?",
  alreadyHaveAnAccount: 'Already have an account?',
  welcomeBack: 'Welcome Back!',
  getStartedHere: 'Get Started Here',
  isRequired: '{name} is required',
  mustBeValid: '{name} must be valid',
  cannotBeMoreThanCharacters: `{name} cannot be more than {number} characters`,
  mustBeAtLeast: `{name} must be at least {number} characters`,
  passwordsMustMatch: 'Passwords must match',
  yourAreNowSignedIn: `You are now signed in!`,
  close: `Close`
}

client\lang\es-ES.js

export default {
  posts: 'Entradas',
  signin: 'Iniciar sesión',
  signup: 'Inscripción',
  searchposts: 'Buscar entradas',
  home: 'Inicio',
  createPost: 'Crear Entrada',
  profile: 'Perfil',
  signout: 'Cerrar sesión',
  username: 'Usuario',
  password: 'Contraseña',
  confirmPassword: 'Confirmar Contraseña',
  email: 'Email',
  dontHaveAnAccount: '¿No tiene una cuenta?',
  alreadyHaveAnAccount: '¿Ya tiene una cuenta?',
  welcomeBack: '¡Bienvenido de nuevo!',
  getStartedHere: 'Comience aquí',
  isRequired: '{name} es requerido',
  mustBeValid: '{name} debe de ser válido',
  cannotBeMoreThanCharacters: `{name} no puede contener más de {number} caracteres`,
  mustBeAtLeast: `{name} debe contener al menos {number} caracteres`,
  passwordsMustMatch: 'Las Contraseñas deben coincidir',
  yourAreNowSignedIn: `¡Ahora está registrado!`,
  close: `Cerrar`
}

7.-Modify the signin page to include the form validation.

  • We need to modify the signin\index.vue page document to include the form validation.

client\pages\signin\index.vue

<template>
  <v-container text-xs-center mt-5 pt-5>
    <!-- Signin Title -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <h1>{{ $t('welcomeBack') }}</h1>
      </v-flex>
    </v-layout>

    <!-- Error Alert -->
    <v-layout v-if="error" row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <form-alert :message="error"></form-alert>
      </v-flex>
    </v-layout>

    <!-- Signin Form -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <v-card color="secondary" dark>
          <v-container>
            <v-form
              ref="form"
              v-model="isFormValid"
              lazy-validation
              @submit.prevent="handleSigninUser"
            >
              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="username"
                    :rules="usernameRules"
                    prepend-icon="face"
                    :label="$t('username')"
                    type="text"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="password"
                    :rules="passwordRules"
                    prepend-icon="extension"
                    :label="$t('password')"
                    type="password"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-btn
                    :loading="loading"
                    :disabled="!isFormValid || loading"
                    color="accent"
                    type="submit"
                  >
                    <span slot="loader" class="custom-loader">
                      <v-icon light>cached</v-icon>
                    </span>
                    {{ $t('signin') }}</v-btn
                  >
                  <h3>
                    {{ $t('dontHaveAnAccount') }}
                    <nuxt-link :to="localePath('signup')">{{
                      $t('signup')
                    }}</nuxt-link>
                  </h3>
                </v-flex>
              </v-layout>
            </v-form>
          </v-container>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Signin',
  data() {
    return {
      isFormValid: false,
      username: '',
      password: '',
      usernameRules: [
        // Check if username in input
        username =>
          !!username ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('username') }),
        // Make sure username is less than 10 characters
        username =>
          username.length <= 10 ||
          this.$i18n.t('cannotBeMoreThanCharacters', {
            name: this.$i18n.t('username'),
            number: 10
          })
      ],
      passwordRules: [
        password =>
          !!password ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('password') }),
        // Make sure password is at least 4 characters
        password =>
          password.length >= 4 ||
          this.$i18n.t('mustBeAtLeast', {
            name: this.$i18n.t('password'),
            number: 4
          })
      ]
    }
  },
  computed: {
    ...mapGetters(['loading', 'error', 'user'])
  },
  watch: {
    user(value) {
      // if user value changes, redirect to home page
      if (value) {
        this.$router.push(this.localePath('index'))
      }
    }
  },
  methods: {
    handleSigninUser() {
      if (this.$refs.form.validate()) {
        this.$store.dispatch('signinUser', {
          username: this.username,
          password: this.password
        })
      }
    }
  }
}
</script>

<style>
.custom-loader {
  animation: loader 1s infinite;
  display: flex;
}
@-moz-keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
@-webkit-keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
@-o-keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
@keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>

8.-Modify the signup page to include to include all the functionality .

  • We need to modify the signup\index.vue page document to include all the functionality to manage the Signup.

client\pages\signup\index.vue

<template>
  <v-container text-xs-center mt-5 pt-5>
    <!-- Signup Title -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <h1>{{ $t('getStartedHere') }}</h1>
      </v-flex>
    </v-layout>

    <!-- Error Alert -->
    <v-layout v-if="error" row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <form-alert :message="error"></form-alert>
      </v-flex>
    </v-layout>

    <!-- Signup Form -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <v-card color="accent" dark>
          <v-container>
            <v-form
              ref="form"
              v-model="isFormValid"
              lazy-validation
              @submit.prevent="handleSignupUser"
            >
              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="username"
                    :rules="usernameRules"
                    prepend-icon="face"
                    :label="$t('username')"
                    type="text"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="email"
                    :rules="emailRules"
                    prepend-icon="email"
                    :label="$t('email')"
                    type="email"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="password"
                    :rules="passwordRules"
                    prepend-icon="extension"
                    :label="$t('password')"
                    type="password"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-text-field
                    v-model="passwordConfirmation"
                    :rules="passwordRules"
                    prepend-icon="gavel"
                    :label="$t('confirmPassword')"
                    type="password"
                    required
                  ></v-text-field>
                </v-flex>
              </v-layout>

              <v-layout row>
                <v-flex xs12>
                  <v-btn
                    :loading="loading"
                    :disabled="!isFormValid || loading"
                    color="info"
                    type="submit"
                  >
                    <span slot="loader" class="custom-loader">
                      <v-icon light>cached</v-icon>
                    </span>
                    {{ $t('signup') }}</v-btn
                  >
                  <h3>
                    {{ $t('alreadyHaveAnAccount') }}
                    <router-link :to="localePath('signin')">{{
                      $t('signin')
                    }}</router-link>
                  </h3>
                </v-flex>
              </v-layout>
            </v-form>
          </v-container>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Signup',
  data() {
    return {
      isFormValid: false,
      username: '',
      email: '',
      password: '',
      passwordConfirmation: '',
      usernameRules: [
        username =>
          !!username ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('username') }),
        username =>
          username.length <= 10 ||
          this.$i18n.t('cannotBeMoreThanCharacters', {
            name: this.$i18n.t('username'),
            number: 10
          })
      ],
      emailRules: [
        email =>
          !!email ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('email') }),
        email =>
          /.@+./.test(email) ||
          this.$i18n.t('mustBeValid', { name: this.$i18n.t('email') })
      ],
      passwordRules: [
        password =>
          !!password ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('password') }),
        password =>
          password.length >= 4 ||
          this.$i18n.t('mustBeAtLeast', {
            name: this.$i18n.t('password'),
            number: 4
          }),
        confirmation =>
          confirmation === this.password || this.$i18n.t('passwordsMustMatch')
      ]
    }
  },
  computed: {
    ...mapGetters(['loading', 'error', 'user'])
  },
  watch: {
    user(value) {
      // if user value changes, redirect to home page
      if (value) {
        this.$router.push(this.localePath('index'))
      }
    }
  },
  methods: {
    handleSignupUser() {
      if (this.$refs.form.validate()) {
        this.$store.dispatch('signupUser', {
          username: this.username,
          email: this.email,
          password: this.password
        })
      }
    }
  }
}
</script>

<style>
.custom-loader {
  animation: loader 1s infinite;
  display: flex;
}
@-moz-keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
@-webkit-keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
@-o-keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
@keyframes loader {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>

9.-Modify the main default layout to include the AuthSnackbar and AuthErrorSnackbar components

  • We need to modify the default.vue layout document to include the AuthSnackbar and AuthErrorSnackbar components.

client\layouts\default.vue

<template>
  <v-app style="background: #E3E3EE">
    <!-- Side Navbar -->
    <v-navigation-drawer v-model="sideNav" fixed app temporary>
      <v-toolbar color="accent" dark flat>
        <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
        <nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
          <h1 class="title pl-3">VueShare</h1>
        </nuxt-link>
      </v-toolbar>

      <v-divider></v-divider>

      <!-- Side Navbar Links -->
      <v-list>
        <v-list-tile
          v-for="(item, i) in sideNavItems"
          :key="i"
          :to="localePath(item.link)"
          ripple
        >
          <v-list-tile-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            {{ item.title }}
          </v-list-tile-content>
        </v-list-tile>

        <!-- Signout Button -->
        <v-list-tile v-if="user" @click="handleSignoutUser">
          <v-list-tile-action>
            <v-icon>exit_to_app</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>{{ $t('signout') }}</v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>

    <!-- Horizontal Navbar -->
    <v-toolbar fixed color="primary" dark>
      <!-- App Title -->
      <v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
          VueShare
        </nuxt-link>
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <!-- Search Input -->
      <v-text-field
        flex
        prepend-icon="search"
        color="accent"
        single-line
        hide-details
        :placeholder="$t('searchposts')"
      ></v-text-field>

      <v-spacer></v-spacer>

      <!-- Horizontal Navbar Links -->
      <v-toolbar-items class="hidden-xs-only">
        <v-btn
          v-for="(item, i) in horizontalNavItems"
          :key="i"
          :to="localePath(item.link)"
          flat
        >
          <v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
          {{ item.title }}
        </v-btn>
      </v-toolbar-items>

      <!-- Profile Button -->
      <v-btn v-if="user" flat :to="localePath('profile')">
        <v-icon class="hidden-sm-only" left>account_box</v-icon>
        <v-badge right color="blue darken-2">
          <!-- <span slot="badge"></span> -->
          {{ $t('profile') }}
        </v-badge>
      </v-btn>

      <!-- Signout Button -->
      <v-btn v-if="user" flat @click="handleSignoutUser">
        <v-icon class="hidden-sm-only" left>exit_to_app</v-icon>
        {{ $t('signout') }}
      </v-btn>

      <v-toolbar-title class="hidden-xs-only">
        <nuxt-link
          v-for="(locale, i) in showLocales"
          :key="i"
          tag="span"
          style="cursor: pointer"
          class="lang-switcher"
          :to="switchLocalePath(locale.code)"
        >
          {{ locale.name }}
        </nuxt-link>
      </v-toolbar-title>
    </v-toolbar>

    <!-- App Content -->
    <main>
      <v-container class="mt-4">
        <transition name="fade">
          <nuxt />
        </transition>

        <!-- Auth Snackbar -->
        <v-snackbar
          v-model="authSnackbar"
          color="success"
          :timeout="5000"
          bottom
          left
        >
          <v-icon class="mr-3">check_circle</v-icon>
          <h3>{{ $t('yourAreNowSignedIn') }}</h3>
          <v-btn dark flat @click="authSnackbar = false">{{
            $t('close')
          }}</v-btn>
        </v-snackbar>

        <!-- Auth Error Snackbar -->
        <v-snackbar
          v-if="authError"
          v-model="authErrorSnackbar"
          color="info"
          :timeout="5000"
          bottom
          left
        >
          <v-icon class="mr-3">cancel</v-icon>
          <h3>{{ authError.message }}</h3>
          <v-btn dark flat :to="localePath('signin')">{{ $t('close') }}</v-btn>
        </v-snackbar>
      </v-container>
    </main>
  </v-app>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  head() {
    return this.$nuxtI18nSeo()
  },
  data() {
    return {
      sideNav: false,
      authSnackbar: false,
      authErrorSnackbar: false
    }
  },
  computed: {
    ...mapGetters(['authError', 'user']),
    horizontalNavItems() {
      let items = [
        { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
        { icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
        { icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
      ]
      if (this.user) {
        items = [{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' }]
      }
      return items
    },
    sideNavItems() {
      let items = [
        { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
        { icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
        { icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
      ]
      if (this.user) {
        items = [
          { icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
          {
            icon: 'stars',
            title: this.$i18n.t('createPost'),
            link: 'posts-add'
          },
          {
            icon: 'account_box',
            title: this.$i18n.t('profile'),
            link: 'profile'
          }
        ]
      }
      return items
    },
    showLocales() {
      return this.$i18n.locales.filter(
        locale => locale.code !== this.$i18n.locale
      )
    }
  },
  watch: {
    user(newValue, oldValue) {
      // if we had no value for user before, show snackbar
      if (oldValue === null) {
        this.authSnackbar = true
      }
    },
    authError(value) {
      // if auth error is not null, show auth error snackbar
      if (value !== null) {
        this.authErrorSnackbar = true
      }
    }
  },
  methods: {
    handleSignoutUser() {
      this.$store.dispatch('signoutUser')
    },
    toggleSideNav() {
      this.sideNav = !this.sideNav
    }
  }
}
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition-property: opacity;
  transition-duration: 0.25s;
}

.fade-enter-active {
  transition-delay: 0.25s;
}

.fade-enter,
.fade-leave-active {
  opacity: 0;
}
</style>

10.-We need to test if it works.

Section 10: Add Post / Infinite Scroll Components

I.-Modify the server app to include the infiniteScrollPosts query.

1.-Create the new PostPage type

  • We are going to create the post-page.type.ts document with the new PostPage type.

server\src\posts\graphql\types\post-page.type.ts

import { Field, ObjectType } from 'type-graphql'
import { Post } from '../post.model'
import { Ref } from 'typegoose'

@ObjectType()
export class PostPage {
  @Field(() => [Post], { nullable: "items" })
  readonly posts: Ref<Post>[]

  @Field(() => Boolean)
  hasMore: boolean
}

2.-Modify the Posts service to include the new infiniteScrollPosts method.

  • We need to modify the \posts.service.ts service document to include the new infiniteScrollPosts method.

server\src\posts\posts.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from 'nestjs-typegoose';
import { Post } from './model/post.model';
import { PostPage } from './graphql/types/post-page.type';
import { CreatePostDto } from './model/dtos/create-post.dto'
import { ModelType } from 'typegoose';

@Injectable()
export class PostsService {
  constructor(@InjectModel(Post) private readonly postModel: ModelType<Post>) { }

  async getPosts(): Promise<Post[] | null> {
    const posts = await this.postModel.find({})
      .sort({ createdDate: "desc" })
      .populate({
        path: "createdBy",
        model: "User"
      });
    return posts;
  }

  async infiniteScrollPosts(pageNum: number, pageSize: number): Promise<PostPage | null> {
    const skips = pageSize * (pageNum - 1)
    const posts = await this.postModel.find({})
      .sort({ createdDate: "desc" })
      .populate({
        path: "createdBy",
        model: "User"
      })
      .skip(skips)
      .limit(pageSize)
      .lean()
      const totalDocs = await this.postModel.countDocuments()
      const hasMore = totalDocs > pageSize * pageNum
      const postPage: PostPage = {
        posts,
        hasMore
      }
      return postPage
  }  

  async addPost(createPostDto: CreatePostDto): Promise<Post> {
    const newPost = new this.postModel(createPostDto);
    return await newPost.save();
  }
}

3.-Modify the Posts resolver to include the new infiniteScrollPosts Query.

  • We need to modify the \posts.resolver.ts resolver document to include the new infiniteScrollPosts Query.

server\src\posts\graphql\posts.resolver.ts

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { Int, ID } from 'type-graphql';
import { PostsService } from '../posts.service';
import { PostInput } from './inputs/post.input';
import { PostPage } from './types/post-page.type';
import { Post } from './types/post.type';

@Resolver()
export class PostsResolver {
  constructor(private readonly postsService: PostsService) { }

  @Query(() => [Post])
  async getPosts() {
    return await this.postsService.getPosts();
  }

  @Query(() => PostPage)
  async infiniteScrollPosts(
    @Args({ name: 'pageNum', type: () => Int}) pageNum: number,
    @Args({ name: 'pageSize', type: () => Int}) pageSize: number
  ) {
    return await this.postsService.infiniteScrollPosts(pageNum, pageSize);
  }

  @Mutation(() => Post)
  async addPostWithInput(@Args('input') input: PostInput) {
    const createdBy = input.creatorId;
    const { title, imageUrl, categories, description } = input
    return await this.postsService.addPost({ title, imageUrl, categories, description, createdBy });
  }
  @Mutation(() => Post)
  async addPost(
    @Args('title') title: string,
    @Args('imageUrl') imageUrl: string,
    @Args({ name: 'categories', type: () => [String], nullable: "items" }) categories: string[],
    @Args('description') description: string,
    @Args({ name: 'creatorId', type: () => ID}) creatorId: string,
  ) {
    const createdBy = creatorId;
    return await this.postsService.addPost({ title, imageUrl, categories, description, createdBy });
  }

}

4.-Test if the new infiniteScrollPosts Query works propely

query

  query($pageNum: Int!, $pageSize: Int!) {
    infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
      hasMore
      posts {
        _id
        title
        imageUrl
        categories
        description
        likes
        createdDate
        messages {
          _id
        }
        createdBy {
          _id
          username
          avatar
        }
      }
    }
  }

variables

{
  "pageNum": 1,
  "pageSize": 3
}

response

{
  "data": {
    "infiniteScrollPosts": {
      "hasMore": true,
      "posts": [
        {
          "_id": "5d4fa77cfe7af92694bf3cae",
          "title": "Abstract Painting",
          "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Vassily_Kandinsky%2C_1939_-_Composition_10.jpg/1024px-Vassily_Kandinsky%2C_1939_-_Composition_10.jpg",
          "categories": [
            "Art"
          ],
          "description": "Nice painting",
          "likes": 0,
          "createdDate": "2019-08-11T05:28:28.697Z",
          "messages": [],
          "createdBy": {
            "_id": "5d35390f58729c10f875d41c",
            "username": "Fofo",
            "avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon"
          }
        },
        {
          "_id": "5d4fa31dcb16795b68483e10",
          "title": "At the Beach",
          "imageUrl": "https://images.pexels.com/photos/1139541/pexels-photo-1139541.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
          "categories": [
            "Photography",
            "Travel"
          ],
          "description": "A nice photo of the waves",
          "likes": 0,
          "createdDate": "2019-08-11T05:09:49.634Z",
          "messages": [],
          "createdBy": {
            "_id": "5d35390f58729c10f875d41c",
            "username": "Fofo",
            "avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon"
          }
        },
        {
          "_id": "5d0e10be7dd57444d0607790",
          "title": "Tasty coffee",
          "imageUrl": "https://images.pexels.com/photos/374757/pexels-photo-374757.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
          "categories": [
            "Art",
            "Food"
          ],
          "description": "Some nice coffee artwork",
          "likes": 0,
          "createdDate": "2019-06-22T11:27:58.416Z",
          "messages": null,
          "createdBy": {
            "_id": "5d36a9e007bced421c3a2031",
            "username": "John",
            "avatar": "http://gravatar.com/avatar/61409aa1fd47d4a5332de23cbf59a36f?d=identicon"
          }
        }
      ]
    }
  }
}

II.-Modify the client app to make the Add Posts component using the new infiniteScrollPosts Query.

1.-Create the infiniteScrollPosts.gql and addPost.gql documents with the infiniteScrollPosts Query and addPost mutation.

  • We need to create the addPost.gql document with the addPost Mutation

client\gql\addPost.gql

mutation addPost(
  $title: String!
  $imageUrl: String!
  $categories: [String]!
  $description: String!
  $creatorId: ID!
) {
  addPost(
    title: $title
    imageUrl: $imageUrl
    categories: $categories
    description: $description
    creatorId: $creatorId
  ) {
    _id
    title
    imageUrl
    categories
    description
  }
}

  • We need to create the infiniteScrollPosts.gql document with the new infiniteScrollPosts Query

client\gql\infiniteScrollPosts.gql

  • We are going to modify the `` document to put only the fields that we really need.

client\gql\getPosts.gql

query getPosts {
  getPosts {
    _id
    title
    imageUrl
  }
} 

2.-Update the localization documents with new entries

client\lang\en-US.js

export default {
  posts: 'Posts',
  signin: 'Sign in',
  signup: 'Sign up',
  searchposts: 'Search Posts',
  home: 'Home',
  createPost: 'Create Post',
  profile: 'Profile',
  signout: 'Signout',
  username: 'Username',
  password: 'Password',
  confirmPassword: 'Confirm Password',
  email: 'Email',
  dontHaveAnAccount: "Don't have an account?",
  alreadyHaveAnAccount: 'Already have an account?',
  welcomeBack: 'Welcome Back!',
  getStartedHere: 'Get Started Here',
  isRequired: '{name} is required',
  mustBeValid: '{name} must be valid',
  cannotBeMoreThanCharacters: '{name} cannot be more than {number} characters',
  mustBeAtLeast: '{name} must be at least {number} characters',
  passwordsMustMatch: 'Passwords must match',
  yourAreNowSignedIn: 'You are now signed in!',
  close: 'Close',
  sessionExpiredSignInAgain: 'Your session has ended. Please sign in again.',
  addPost: 'Add Post',
  postTitle: 'Post Title',
  imageUrl: 'Image URL',
  category: 'Category | Categories',
  categoryItems: {
    art: 'Art',
    education: 'Education',
    food: 'Food',
    furniture: 'Furniture',
    travel: 'Travel',
    photography: 'Photography',
    technology: 'Technology'
  },
  description: 'Description',
  submit: 'Submit',
  atLeastOne: 'At least one',
  likes: 'likes',
  comments: 'comments',
  added: 'Added',
  fetchMore: 'Fetch More'
}

client\lang\es-ES.js

export default {
  posts: 'Entradas',
  signin: 'Iniciar sesión',
  signup: 'Inscripción',
  searchposts: 'Buscar entradas',
  home: 'Inicio',
  createPost: 'Crear Entrada',
  profile: 'Perfil',
  signout: 'Cerrar sesión',
  username: 'Usuario',
  password: 'Contraseña',
  confirmPassword: 'Confirmar Contraseña',
  email: 'Email',
  dontHaveAnAccount: '¿No tiene una cuenta?',
  alreadyHaveAnAccount: '¿Ya tiene una cuenta?',
  welcomeBack: '¡Bienvenido de nuevo!',
  getStartedHere: 'Comience aquí',
  isRequired: '{name} es requerido',
  mustBeValid: '{name} debe de ser válido',
  cannotBeMoreThanCharacters:
    '{name} no puede contener más de {number} caracteres',
  mustBeAtLeast: '{name} debe contener al menos {number} caracteres',
  passwordsMustMatch: 'Las Contraseñas deben coincidir',
  yourAreNowSignedIn: '¡Ahora está registrado!',
  close: 'Cerrar',
  sessionExpiredSignInAgain:
    'Su sesión ha caducado. Por favor, inicie la sesión otra vez.',
  addPost: 'Añadir Entrada',
  postTitle: 'Título de la entrada',
  imageUrl: 'URL de la imagen',
  category: 'Categoría | Categorías',
  categoryItems: {
    art: 'Arte',
    education: 'Educación',
    food: 'Comida',
    furniture: 'Muebles',
    travel: 'Viajes',
    photography: 'Fotografía',
    technology: 'Tecnología'
  },
  description: 'Descripción',
  submit: 'Enviar',
  atLeastOne: 'Por lo menos una',
  likes: 'me gusta',
  comments: 'comentarios',
  added: 'Creado',
  fetchMore: 'Obtener más'
}

3.-Modify the store to include the addPost action

  • We need to modify the store\index.js store document to include the addPost action and mutation.

client\store\index.js

import { getPosts } from '~/gql/getPosts.gql'
import { getCurrentUser } from '~/gql/getCurrentUser.gql'
import { signinUser } from '~/gql/signinUser.gql'
import { signupUser } from '~/gql/signupUser.gql'
import { addPost } from '~/gql/addPost.gql'
import utils from '~/helpers/utils'

export const state = () => ({
  posts: [],
  user: null,
  loading: false,
  error: null,
  authError: null
})
export const mutations = {
  setPosts: (state, payload) => {
    state.posts = payload
  },
  addPost: (state, payload) => {
    const posts = state.posts
    posts.unshift(payload)
    state.posts = posts
  },
  setUser: (state, payload) => {
    state.user = payload
  },
  setLoading: (state, payload) => {
    state.loading = payload
  },
  clearUser: state => (state.user = null),
  setError: (state, payload) => {
    state.error = payload
  },
  clearError: state => (state.error = null),
  setAuthError: (state, payload) => {
    state.authError = payload
  },
  clearAuthError: state => (state.authError = null)
}
export const actions = {
  async nuxtServerInit({ dispatch }) {
    await dispatch('getCurrentUser')
    await dispatch('getPosts')
  },
  async getPosts({ commit }) {
    commit('clearError')
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.query({
        query: getPosts
      })
      commit('setPosts', result.data.getPosts)
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      commit('setError', currentError)
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async addPost({ commit }, payload) {
    commit('clearError')
    commit('setLoading', true)
    try {
      await this.app.apolloProvider.defaultClient.mutate({
        mutation: addPost,
        variables: payload
      })
      const { _id, title, imageUrl } = payload
      const newPost = {
        _id,
        title,
        imageUrl
      }
      commit('addPost', newPost)
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      commit('setError', currentError)
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async logOut({ commit }) {
    this.app.$cookies.remove('apollo-token')
    await this.app.$apolloHelpers.onLogout()
    commit('clearUser')
  },
  async getCurrentUser({ commit, dispatch }) {
    if (!utils.isJwtTokenValid(this.app.$apolloHelpers.getToken())) {
      await dispatch('logOut')
      return
    }
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.query({
        query: getCurrentUser
      })
      // Add user data to state
      commit('setUser', result.data.getCurrentUser)
      commit('clearError')
      commit('clearAuthError')
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      if (currentError === 'Unauthorized') {
        commit('setAuthError', this.app.i18n.t('sessionExpiredSignInAgain'))
        await dispatch('logOut')
      } else {
        commit('setError', currentError)
      }
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async signinUser({ commit, dispatch }, payload) {
    commit('clearError')
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.mutate({
        mutation: signinUser,
        variables: payload
      })
      await this.app.$apolloHelpers.onLogin(result.data.signinUser.token)
      await dispatch('getCurrentUser')
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      commit('setError', currentError)
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async signupUser({ commit, dispatch }, payload) {
    commit('clearError')
    commit('setLoading', true)
    try {
      const result = await this.app.apolloProvider.defaultClient.mutate({
        mutation: signupUser,
        variables: payload
      })
      await this.app.$apolloHelpers.onLogin(result.data.signupUser.token)
      await dispatch('getCurrentUser')
    } catch (error) {
      const currentError = utils.getCurrentGraphQLError(error)
      commit('setError', currentError)
      console.error(utils.getFirstGraphQLError(error))
    }
    commit('setLoading', false)
  },
  async signoutUser({ dispatch }) {
    await dispatch('logOut')
    // redirect home - kick users out of private pages (i.e. profile)
    this.app.router.push(this.app.localePath('index'))
  }
}
export const getters = {
  posts: state => state.posts,
  user: state => state.user,
  loading: state => state.loading,
  error: state => state.error,
  authError: state => state.authError
}

4.-Modify the post Add page to include all the functionality .

  • We need to modify the post\add.vue page document to include all the functionality to manage the creation of a new Post

client\pages\posts\add.vue

<template>
  <v-container text-xs-center mt-5 pt-5>
    <!-- Add Post Title -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <h1 class="primary--text">{{ $t('addPost') }}</h1>
      </v-flex>
    </v-layout>

    <!-- Error Alert -->
    <v-layout v-if="error" row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <form-alert :message="error"></form-alert>
      </v-flex>
    </v-layout>

    <!-- Add Post Form -->
    <v-layout row wrap>
      <v-flex xs12 sm6 offset-sm3>
        <v-form
          ref="form"
          v-model="isFormValid"
          lazy-validation
          @submit.prevent="handleAddPost"
        >
          <!-- Title Input -->
          <v-layout row>
            <v-flex xs12>
              <v-text-field
                v-model="title"
                :rules="titleRules"
                :label="$t('postTitle')"
                type="text"
                required
              ></v-text-field>
            </v-flex>
          </v-layout>

          <!-- Image Url Input -->
          <v-layout row>
            <v-flex xs12>
              <v-text-field
                v-model="imageUrl"
                :rules="imageRules"
                :label="$t('imageUrl')"
                type="text"
                required
              ></v-text-field>
            </v-flex>
          </v-layout>

          <!-- Image Preview -->
          <v-layout row>
            <v-flex xs12>
              <img :src="imageUrl" height="300px" />
            </v-flex>
          </v-layout>

          <!-- Categories Select -->
          <v-layout row>
            <v-flex xs12>
              <v-select
                v-model="categories"
                :rules="categoriesRules"
                item-text="text"
                item-value="value"
                :items="categoryItems"
                multiple
                :label="$tc('category', 2)"
              ></v-select>
            </v-flex>
          </v-layout>

          <!-- Description Text Area -->
          <v-layout row>
            <v-flex xs12>
              <v-textarea
                v-model="description"
                :rules="descRules"
                :label="$t('description')"
                type="text"
                required
              ></v-textarea>
            </v-flex>
          </v-layout>

          <v-layout row>
            <v-flex xs12>
              <v-btn
                :loading="loading"
                :disabled="!isFormValid || loading"
                color="info"
                type="submit"
              >
                <span slot="loader" class="custom-loader">
                  <v-icon light>cached</v-icon>
                </span>
                {{ $t('submit') }}</v-btn
              >
            </v-flex>
          </v-layout>
        </v-form>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'AddPost',
  middleware: 'auth',
  data() {
    return {
      isFormValid: true,
      title: '',
      imageUrl: '',
      categories: [],
      categoryItems: [
        { value: 'Art', text: this.$i18n.t('categoryItems.art') },
        { value: 'Education', text: this.$i18n.t('categoryItems.education') },
        { value: 'Food', text: this.$i18n.t('categoryItems.food') },
        { value: 'Furniture', text: this.$i18n.t('categoryItems.furniture') },
        { value: 'Travel', text: this.$i18n.t('categoryItems.travel') },
        {
          value: 'Photography',
          text: this.$i18n.t('categoryItems.photography')
        },
        {
          value: 'Technology',
          text: this.$i18n.t('categoryItems.technology')
        }
      ],
      description: '',
      titleRules: [
        title =>
          !!title ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('postTitle') }),
        title =>
          title.length <= 20 ||
          this.$i18n.t('cannotBeMoreThanCharacters', {
            name: this.$i18n.t('postTitle'),
            number: 20
          })
      ],
      imageRules: [
        image =>
          !!image ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('imageUrl') })
      ],
      categoriesRules: [
        categories =>
          categories.length >= 1 ||
          this.$i18n.t('isRequired', {
            name: `${this.$i18n.t('atLeastOne')} ${this.$i18n.tc(
              'category',
              1
            )}`
          })
      ],
      descRules: [
        desc =>
          !!desc ||
          this.$i18n.t('isRequired', { name: this.$i18n.t('description') }),
        desc =>
          desc.length <= 200 ||
          this.$i18n.t('cannotBeMoreThanCharacters', {
            name: this.$i18n.t('description'),
            number: 200
          })
      ]
    }
  },
  computed: {
    ...mapGetters(['loading', 'error', 'user'])
  },
  methods: {
    handleAddPost() {
      if (this.$refs.form.validate()) {
        // add post action
        this.$store.dispatch('addPost', {
          title: this.title,
          imageUrl: this.imageUrl,
          categories: this.categories,
          description: this.description,
          creatorId: this.user._id
        })
        this.$router.push(this.localePath('index'))
      }
    }
  }
}
</script>

5.-Modify the posts page to include all the functionality .

  • We need to modify the post\index.vue page document to include all the functionality to show all the posts incrementally.

client\pages\posts\index.vue

<template>
  <v-container fluid grid-list-xl>
    <!-- Post Cards -->
    <v-layout v-if="infiniteScrollPosts" row wrap>
      <v-flex
        v-for="post in infiniteScrollPosts.posts"
        :key="post._id"
        xs12
        sm6
      >
        <v-card hover>
          <v-img :src="post.imageUrl" height="30vh" lazy></v-img>

          <v-card-actions>
            <v-card-title primary>
              <div>
                <div class="headline">{{ post.title }}</div>
                <span class="grey--text"
                  >{{ post.likes }} {{ $t('likes') }} -
                  {{ post.messages ? post.messages.length : 0 }}
                  {{ $t('comments') }}</span
                >
              </div>
            </v-card-title>
            <v-spacer></v-spacer>
            <v-btn icon @click="showPostCreator = !showPostCreator">
              <v-icon>{{
                `keyboard_arrow_${showPostCreator ? 'up' : 'down'}`
              }}</v-icon>
            </v-btn>
          </v-card-actions>

          <!-- Post Creator Tile -->
          <v-slide-y-transition>
            <v-card-text v-show="showPostCreator" class="grey lighten-4">
              <v-list-tile avatar>
                <v-list-tile-avatar>
                  <img :src="post.createdBy.avatar" />
                </v-list-tile-avatar>

                <v-list-tile-content>
                  <v-list-tile-title class="text--primary">{{
                    post.createdBy.username
                  }}</v-list-tile-title>
                  <v-list-tile-sub-title class="font-weight-thin"
                    >{{ $t('added') }}
                    {{ post.createdDate }}</v-list-tile-sub-title
                  >
                </v-list-tile-content>

                <v-list-tile-action>
                  <v-btn icon ripple>
                    <v-icon color="grey lighten-1">info</v-icon>
                  </v-btn>
                </v-list-tile-action>
              </v-list-tile>
            </v-card-text>
          </v-slide-y-transition>
        </v-card>
      </v-flex>
    </v-layout>

    <!-- Fetch More Button -->
    <v-layout v-if="showMoreEnabled" column>
      <v-flex xs12>
        <v-layout justify-center row>
          <v-btn color="info" @click="showMorePosts">{{
            $t('fetchMore')
          }}</v-btn>
        </v-layout>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import { infiniteScrollPosts } from '~/gql/infiniteScrollPosts.gql'

const pageSize = 2

export default {
  name: 'Posts',
  data() {
    return {
      pageNum: 1,
      showMoreEnabled: true,
      showPostCreator: false
    }
  },
  apollo: {
    infiniteScrollPosts: {
      query: infiniteScrollPosts,
      variables: {
        pageNum: 1,
        pageSize
      }
    }
  },
  methods: {
    showMorePosts() {
      this.pageNum += 1
      // fetch more data and transform original result
      this.$apollo.queries.infiniteScrollPosts.fetchMore({
        variables: {
          // pageNum incremented by 1
          pageNum: this.pageNum,
          pageSize
        },
        updateQuery: (prevResult, { fetchMoreResult }) => {
          console.log('previous result', prevResult.infiniteScrollPosts.posts)
          console.log('fetch more result', fetchMoreResult)

          const newPosts = fetchMoreResult.infiniteScrollPosts.posts
          const hasMore = fetchMoreResult.infiniteScrollPosts.hasMore
          this.showMoreEnabled = hasMore

          return {
            infiniteScrollPosts: {
              __typename: prevResult.infiniteScrollPosts.__typename,
              // Merge previous posts with new posts
              posts: [...prevResult.infiniteScrollPosts.posts, ...newPosts],
              hasMore
            }
          }
        }
      })
    }
  }
}
</script>

6.-We need to test if it works.

Section 11: Post Component

I.-Modify the server app to include the getPost Query and the addPostMessage mutation.

  • The folder structure for the models have been changed. Basically the GraphQL and Database(MongoDB) models have been merged.

Old format

Level 1 Level 2 Level 3 Description
graphql Everthing related to GraphQL
- inputs input objects used in GraphQL Queries and Mutations
- post-input.ts input object used when creating a post
- types object types used by GraphQL
- post-page.type.ts PostPage type
- post.type.ts Post type
- posts.resolver.ts GraphQL Post resolver
model Everthing related to database model
- dtos dtos used mainly for service input objects
- create-post-message.dto.ts CreatePostMessage dto
- create-post.dto.ts CreatePost dto
- post.model.ts Database Post model

New format

Level 1 Level 2 Description
dtos dtos used mainly for service input objects
- create-post-message.dto.ts CreatePostMessage dto
- create-post.dto.ts CreatePost dto
inputs input objects used in GraphQL Queries and Mutations
- post-input.ts input object used when creating a post
post.model.ts Database and GraphQL Post model
posts.resolver.ts GraphQL Post resolver

1.-Update the posts module with the new structure and add the getPost Query and the addPostMessage mutation.

server\src\posts\dtos\create-post-message.dto.ts

export class CreatePostMessageDto {
  readonly messageBody: string;
  readonly userId: string;
  readonly postId: string;
}

server\src\posts\dtos\create-post.dto.ts

export class CreatePostDto {
  readonly title: string;
  readonly imageUrl: string;
  readonly categories: string[];
  readonly description: string;
  readonly createdBy: string;
}

server\src\posts\inputs\post.input.ts

import { InputType, Field, ID } from 'type-graphql';

@InputType()
export class PostInput {
  @Field()
  readonly title: string;
  @Field()
  readonly imageUrl: string;
  @Field(() => [String], { nullable: "items" })
  readonly categories: string[];
  @Field()
  readonly description: string;
  @Field(() => ID)
  readonly creatorId: string;

}

server\src\posts\types\post-page.type.ts

import { Field, ObjectType } from 'type-graphql'
import { Post } from '../post.model'
import { Ref } from 'typegoose'

@ObjectType()
export class PostPage {
  @Field(() => [Post], { nullable: "items" })
  readonly posts: Ref<Post>[]

  @Field(() => Boolean)
  hasMore: boolean
}

server\src\posts\post.model.ts

import { prop, arrayProp, Typegoose, Ref } from 'typegoose';
import { IsDate, IsInt } from 'class-validator';
import { Field, ObjectType, ID, Int } from 'type-graphql';
import { User } from '../users/user.model'

@ObjectType()
export class Message extends Typegoose {
  @Field(() => ID)
  _id: string

  @Field()
  @prop({ required: true })
  messageBody: string;

  @IsDate()
  @Field()
  @prop({ default: Date.now })
  messageDate: Date;

  @Field(() => User)
  @prop({ required: true, ref: User })
  messageUser: Ref<User>
}

@ObjectType()
export class Post extends Typegoose {
  @Field(() => ID)
  _id: string

  @Field()
  @prop({ required: true })
  title: string; 

  @Field()
  @prop({ required: true })
  imageUrl: string;

  @Field(() => [String], { nullable: "items" })
  @prop({ required: true })
  categories: string[];

  @Field()
  @prop({ required: true })
  description: string;

  @IsDate()
  @Field()
  @prop({ default: Date.now })
  createdDate: Date

  @IsInt()
  @Field(() => Int, { nullable: true })
  @prop({ default: 0 })
  likes: number;

  @Field(() =>