Chapter 2: TypeGraphQL

Strongly Typed Next.js

link TypeGraphQL

By the end of this lesson, developers will be able to:

  • Create a GraphQL API schema with TypeGraphQL
  • Create a UserResolver for fetching user data

Introduction

Authentication is one of the most challenging tasks for developers just starting with GraphQL. There are a lot of technical considerations, including what ORM would be easy to set up, how to generate secure tokens and hash passwords, and even what HTTP library to use and how to use it.

In this section, we will start building a GraphQL API server. With MongoDB as our database, we learn how to incorporate TypeGraphQL and Typegoose. Let's begin with an overview of these libraries and their dependencies:

  • TypeGraphQL: We will use TypeGraphQL to declare our models and their properties.
  • Typegoose: Once the models are declared, we will use Typegoose to manage how MongoDB works in the database.
  • jsonwebtoken: Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. jsonwebtoken will be used to generate a JWT which will be used to authenticate users.

TypeGraphQL

Starting to incorporate Typescript and GraphQL, we can utilize existing libraries for creating strongly typed schemas. For a brief introduction to TypeGraphQL, be sure to visit the TypeGraphQL documentation.

TypeGraphQL solves many problems for us, like schema validation, authorization and dependency injection, which helps develop GraphQL APIs quickly and easily. TypeGraphQL also integrates with several third party libraries like Typegoose.

link Entities

In order to design a relational database, we need to understand the connections between our models. Let's define an Entity Relationship Diagram, or ERD for our models:

As seen above, each User can create many Stream entities. We will continue to define the backend models with TypeGraphQL and Typegoose.

link Installation

First, create a new directory at the root of your project and cd into it:

mkdir api
cd api

Let's begin by installing each library and their peer dependencies.

npm init -y

TypeGraphQL

npm install typescript type-graphql graphql reflect-metadata
npm install --save-dev @types/node 

Typegoose

npm install @typegoose/typegoose mongoose connect-mongo
npm install --save-dev @types/mongoose

Express and JWT

npm install express jsonwebtoken
npm install --save-dev @types/express @types/jsonwebtoken

TypeScript Config

To create a TypeScript configuration file, you can run the following command (similar to npm init -y):

npx tsc --init

You will receive this output:

Output
message TS6071: Successfully created a tsconfig.json file.

Open your new api/tsconfig.json file and you will see lots of different options, most of which are commented out.

Replace your existing api/tsconfig.json file with the following contents:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
    "sourceMap": true,
    "outDir": "./dist",
    "moduleResolution": "node",
    "removeComments": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "resolveJsonModule": true,
    "baseUrl": "."
  },
  "exclude": ["node_modules", "./generated"],
  "include": ["./**/*.ts"]
}

TypeScript has gained popularity over the last couple of years. In modern front-end frameworks, TypeScript has become a first-class citizen.

For more information on TypeScript and Visual Studio Code, visit digitalocean.com

link User Entity

Let's prepare our database schema with our first entity, called User.

Create a new directory and file, api/entity and api/entity/User.ts and insert the following:

import { prop as Property, getModelForClass } from "@typegoose/typegoose";
import { ObjectId } from "mongodb";
import { Field, ObjectType } from "type-graphql";

@ObjectType()
export class User {
  @Field()
  readonly _id: ObjectId;

  @Field()
  @Property({ required: true })
  email: string;

  @Property({ required: true })
  password: string;
}

export const UserModel = getModelForClass(User);

Each user has two accessible fields: _id and email. Passwords are readonly for security purposes. We will demonstrate how to securely store passwords in an upcoming section.

Decorators

We just wrote our first decorators!

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated class.

For more information on decorators visit typescriptlang.org

Ref Type

Before going on to the Stream entity, let's define a Ref object type for our database. A Ref is considered to be a manual reference. A manual references is where you save the ObjectId field of one document in another document as a reference. Then your application can run a second query to return the related data. These references are simple and sufficient for most use cases.

Create a new directory and file, api/types and api/types/Ref.ts and insert the following:

import { ObjectId } from "mongodb";

export type Ref<T> = T | ObjectId;

Using manual references is the practice of including one document's ObjectId field in another document. The application can then issue a second query to resolve the referenced fields as needed.

For nearly every case where you want to store a relationship between two documents, use manual references. The references are simple to create and your application can resolve references as needed. However, if you need to reference documents from multiple collections, consider using DBRefs.

For more information about Database References, visit MongoDB.org

link Stream Entity

Streams are considered to be embedded posts. We reference the User entity with our Ref type, and assign them as the stream's author.

Create a new file, api/entity/Stream.ts and insert the following:

import { prop as Property, getModelForClass } from "@typegoose/typegoose";
import { ObjectId } from "mongodb";
import { Field, ObjectType } from "type-graphql";
import { User } from "./User";
import { Ref } from "../types/Ref";

@ObjectType()
export class Stream {
  @Field()
  readonly _id: ObjectId;

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

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

  @Field()
  @Property({ required: true })
  url: string;

  @Field(() => User)
  @Property({ ref: User, required: true })
  author: Ref<User>;
}

export const StreamModel = getModelForClass(Stream);

With User and Stream entities defined we can go on to create a new ObjectID scalar.

link ObjectID

Before moving on to the resolvers, we will define an ObjectId scalar for our schema. This scalar is specific to MongoDB, because an ObjectId has a unique format, e.g. ObjectId("adaj130jfsdm10").

We will see a few examples of working with ObjectId scalars using Typegoose.

Create a new directory and file, api/schema/ and api/schema/object-id.scalar.ts and insert the following:

import { GraphQLScalarType, Kind } from "graphql";
import { ObjectId } from "mongodb";

export const ObjectIdScalar = new GraphQLScalarType({
  name: "ObjectId",
  description: "Mongo object id scalar type",
  parseValue(value: string) {
    return new ObjectId(value); // value from the client input variables
  },
  serialize(value: ObjectId) {
    return value.toHexString(); // value sent to the client
  },
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      return new ObjectId(ast.value); // value from the client query
    }
    return null;
  },
});

The GraphQLScalarType code handles parsing objects as strings, and serializing them as hex strings. This is useful for converting ObjectId properties into string values.

In summary ObjectID("adaj130jfsdm10") becomes a text string: adaj130jfsdm10, and vice versa.

link Middleware

MyContext

Before getting started with the resolvers, we will declare a new type: MyContext, which will be used to infer the current user's session.

Create a new file, api/types/MyContext.ts and insert the following:

import { Request, Response } from 'express';

export interface MyContext {
  req: Request;
  res: Response;
}

We will begin to modify MyContext with each session's userId, once we create the authentication resolver.

In order to help create the authentication resolver, we will create a middleware that handles checking for the current user's userId.

isAuth

Let's declare our first middleware, isAuth (or "isAuthenticated") to make sure the current session contains a logged in user.

Create a new directory and file, api/middleware and api/middleware/isAuth.ts and insert the following:

import { MiddlewareFn } from 'type-graphql';
import { MyContext } from '../types/MyContext';
import jwt from 'jsonwebtoken';

const APP_SECRET = process.env.SESSION_SECRET || 'aslkdfjoiq12312';

export const isAuth: MiddlewareFn<MyContext> = async ({ context }, next) => {
  const authorization = context.req.headers['authorization'];
  try {
    const token = authorization?.replace('Bearer ', '')!;
    const user = jwt.verify(token, APP_SECRET) as any;
    context.res.locals.userId = user.id;
    return next();
  } catch (err) {
    throw new Error(err.message);
  }
};

In the above code, we throw an error if the current session contains no userId property. The isAuth middleware will be applied to a few resolvers, as we will see in a few moments.

Similar to Express middleware, TypegraphQL allows us to write custom middlewares for each request. So we can add this custom logic before each incoming request.

For more information on res.locals, visit the Express.js documentation.

We have one more middleware for handling Typegoose documents, so let's go ahead and add it now.

Typegoose Middleware

Create a new file, api/middleware/typegoose.ts and insert the following:

import { Model, Document } from 'mongoose';
import { getClassForDocument } from '@typegoose/typegoose';
import { MiddlewareFn } from 'type-graphql';

export const TypegooseMiddleware: MiddlewareFn = async (_, next) => {
  const result = await next();

  if (Array.isArray(result)) {
    return result.map((item) =>
      item instanceof Model ? convertDocument(item) : item
    );
  }

  if (result instanceof Model) {
    return convertDocument(result);
  }

  return result;
};

function convertDocument(doc: Document) {
  const convertedDocument = doc.toObject();
  const DocumentClass = getClassForDocument(doc)!;
  Object.setPrototypeOf(convertedDocument, DocumentClass.prototype);
  return convertedDocument;
}

In the above code, we convert MongoDB Documents into readable objects. Without this middleware, our Ref types would not be able to reference other database objects.

link UserResolver

Without further ado, let's create our first resolver called UserResolver. This resolver will handle any queries related to fetching user data.

Create a new directory and file, api/resolvers/ and api/resolvers/UserResolver.ts and insert the following:

import { Resolver, Query, UseMiddleware, Arg, Ctx } from 'type-graphql';
import { ObjectId } from 'mongodb';
import { MyContext } from '../types/MyContext';
import { isAuth } from '../middleware/isAuth';
import { User, UserModel } from '../entity/User';
import { ObjectIdScalar } from '../schema/object-id.scalar';

@Resolver(() => User)
export class UserResolver {
  @Query(() => User, { nullable: true })
  async user(@Arg('userId', () => ObjectIdScalar) userId: ObjectId) {
    return await UserModel.findById(userId);
  }

  @Query(() => User, { nullable: true })
  @UseMiddleware(isAuth)
  async currentUser(
    @Ctx()
    ctx: MyContext
  ): Promise<User | null> {
    return await UserModel.findById(ctx.res.locals.userId);
  }
}

You may notice the @UseMiddleware decorator, which is used to integrate the isAuth middleware. Using this resolver, we are able to fetch either individual users or the current logged in user.

Congratulations!

Today we wrote our first resolver. In the next section, we implement the authentication and stream resolvers.

link References

format_list_bulleted
help_outline