preface

In the recent project, I am making BFF, Nest. Js and GraphQL, which are two technology stacks. It is a “new” attempt

Of course, this is not the hydrology of pasting an official document and teaching you how to use it, but the hydrology of picking pits

Shoulders of giants

  • type-graphqltypescriptThe definition ofgraphqlschema
  • @nestjs/graphqlThe author inapollo-serverOn the basis of two times of encapsulation
  • data-loaderData aggregation and caching solutionresolver (n+1)The problem of

Application gateways

Here we use a UserModule as an example

Can be achieved by

query UserList() {
  users {
    id
    name
  }
}
Copy the code

get

{
  data: {
    users: [{
      id: "1",
      name: 'name'}}}]Copy the code
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'

@Module({
  imports: [
    GraphQLModule.forRoot({
      path: '/graphql'.typePaths: ['./**/*.graphql'].definitions: {
        path: join(process.cwd(), 'src/graphql.ts'),
        outputAs: 'class',
      },
    }),
    UserModule,
  ]
})
export class AppModule
Copy the code

In this case, graphql.ts is generated by traversing all graphQL Schema files every time the application is started

For example,

type User {
  id: ID!
  name: String
}
Copy the code

Will be generated

export class User {
  id: stringname? :string
}
Copy the code

Then we can use graphql.ts to generate good type definitions when we write resolver and service, but this is a little inconvenient and a little uncustomary

If you want to write typescript definitions first and generate a schema file for graphQL, you need to use type-graphQL

import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'
import { resolve } from 'path'

const schema = resolve(__dirname, 'schema.gql')

@Module({
  imports: [
    GraphQLModule.forRoot({
      path: '/graphql'.autoSchemaFile: schema,
      typePaths: [schema],
    }),
    UserModule,
  ]
})
export class AppModule
Copy the code

At the end of the day, you just write the model

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

export class User {
  @Field((a)= > ID, { nullable: false })
  id: string

  @Field({ nullable: false}) name? : string }Copy the code

The @Field decorator maps to the type of ID in the schema

The type of TS described by the id of Class User

It is worth noting that the string | base types such as Boolean @ Field can be omitted, but the number will default to float, so need to display a statement, the comparison of the pit

Another point is that if it is an enumeration, you need to register it once using registerEnumType

import { registerEnumType } from 'type-graphql'

export enum Enum {
  a,
  b
}

registerEnumType(Enum, {
  name: 'RolesEnum'
})

/ / use
export class User {

  @Field((a)= > Enum, { nullable: false}) name? : Enum }Copy the code

Resolver

A Graphql module in Nest. Js consists of a Resolver and a service

import { Module } from '@nestjs/common'
import { UserResolver } from './user.resolver'
import { UserService } from './user.service'

@Module({
  providers: [
    UserResolver,
    UserService,
  ]
})
export class UserModule {}
Copy the code
import { Args, Resolver, Query } from '@nestjs/graphql'
import { UserService } from './user.service'

@Resolver()
export class UserResolver {
  constructor(private readonly userService: UserService)

  @Query(() => User[], {
    name: 'users'
  })
  public async users(): Promise<User[]> {
    this.userService.xxxxx()
  }
}
Copy the code

Each @query decorator corresponds to a method that defaults to the name of the function as the name of the Query.

When a Query is issued, the corresponding Resolver will invoke the corresponding service processing logic

query users {
  id
  name
}
Copy the code

If you want to query the third field, age, but the age is not in the User data, for example, if you want to call another interface query, you can use @resolveProperty

import { Args, Resolver, ResolveProperty } from '@nestjs/graphql'

...

@Resolver()
export class UserResolver {
  constructor(private readonly userService: UserService)

  @ResolveProperty(() => number)
  public async age(): Promise<number> {
    this.userService.getAge()
  }
}
Copy the code

But don’t forget to add the age field to the Model

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

export class User {
  @Field((a)= > ID, { nullable: false })
  id: string

  @Field({ nullable: false}) name? : string @Field((a)= > Number, { nullable: false}) age? : number }Copy the code

The Resolver will help you merge them when you query them

  query users {
    id
    name
    age
  }
Copy the code
 {
   id: '1'.name: 'xx'.age: 18
 }
Copy the code

DateLoader

Because of the Resolver’s N+1 query problem

Like this.userService.getage () above, it will be executed multiple times. If you are executing some SQL, it may cause performance problems and waste of resources, but it is not a problem.

We use Dataloader to solve this problem

import DataLoader from 'dataloader'

@Injectable(a)export class UserService {
  loader = new DataLoader((a)= >{
    returnSome query operations}) getAge() {this.loader.load()

    // Query multiple this.loader.loadmany ()}}Copy the code

The idea is to place the current event loop request in the process. NextTick to execute

Docker deployment

Since docker does not have permission to write to files, this will cause a problem when starting the application

.RUN node dist/index.js
Copy the code

This will cause Docker to fail to start, so you need to modify the GraphqlModule configuration a little bit

  • Method 1:
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'

const schema = resolve(__dirname, 'schema.gql')

@Module({
  imports: [
    GraphQLModule.forRoot({
      path: '/graphql',
      autoSchemaFile: process.env.NODE_ENV === 'development' ? schema : false,
      typePaths: [schema],
    }),
    UserModule,
  ]
})
export class AppModule
Copy the code

Schema. GQL is generated during development and is turned off in production

Also specify typePaths as schema.gql to resolve this

  • Method 2:
.COPY schema.gql /dist
RUN node dist/index.js
Copy the code

First, use the buildSchema provided by Type-GraphQL. In fact, the Nest.js GraphqlModule is automatically generated using this method for you

import { buildSchema } from "type-graphql";

async function bootstrap() {
  const schema = await buildSchema({
    resolvers: [__dirname + "/**/*.resolver.ts"]});// other initialization code, like creating http server
}

bootstrap();

Copy the code

Copy this file every time you build an image

Permission to verify

In Express, you can intercept request by intermediate key to do permission verification, and in Nest. Js, you can easily implement it using Guards

import { Args, Resolver, ResolveProperty } from '@nestjs/graphql'
import { AuthGuard } from './auth.guard'

...

@Resolver()
@UseGuards(AuthGuard)
export class UserResolver {
  constructor(private readonly userService: UserService)

  @ResolveProperty(() => number)
  public async age(): Promise<number> {
    this.userService.getAge()
  }
}
Copy the code

Because Graphql has this concept of a context that you can use to get the current request

// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { GqlExecutionContext } from '@nestjs/graphql'

@Injectable(a)export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context).getContext()
    const request = context.switchToHttp().getRequest()

    // Do some permission verification
    / / JWT validation
    // request.headers.authorization}}Copy the code

Conversion error response

Because of the use of apollo-server, there is a deep hierarchy of errors sent to the front end every time a Query or Mutation error is reported,

If you want to customize, you can use formatError and formatResponse, but nest. Js does not provide detailed definitions for these fields

You might have to look at the apo-server documentation, although the TMD documentation is only a few lines long

import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'

const schema = resolve(__dirname, 'schema.gql')

@Module({
  imports: [
    GraphQLModule.forRoot({
      path: '/graphql',
      autoSchemaFile: process.env.NODE_ENV === 'development' ? schema : false,
      typePaths: [schema],
      context(ctx) {
        // Add something to the context ctx.req
        ctx.xx  = 1
        return ctx
      }
      formatError(error) {
        return error
      },
      formatResponse(response, {context}){
        // Let's rewrite this
        // Data, errors are not overwritten by the graphQL specification

        return {
          errors: {}
        }

        // ❌ This is not ok
        return {
          name: 1,
          age: 18
        }

        / / ✅
        return {
          data: {
            name: 1,
            age: 18
          }
        }
      }
    }),
    UserModule,
  ]
})
export class AppModule
Copy the code

test

You might want to write a few unit tests or e2E tests, it’s all in the documentation, so I don’t want to be a mover here

The last

Of course, the sadness of stepping on pit is far more than this little bit of writing, this time is also a lot of harvest, continue to refuel