Intro to TypeGraphQL with TypeORM (tutorial showing all the main features)

Published on

TypeGraphQL is a popular library to help you create a GraphQL server, written in TypeScript.

It works very well with TypeORM to access database data.

Initial setup

Before we being, we need to set a few things up. You can skip this section if you are not following along with the code changes.

Postgres (docker-compose)

Here is a docker-compose config to set up Postgres:

docker-compose.yml
version: '3.5'

services:

  graphql-database:
    image: postgres
    container_name: graphql-database
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    volumes:
      - ./postgresql-data:/var/lib/postgresql/data
    ports:
      - '5432:5432'

Run it with docker-compose up.

TypeORM config

Config file to connect to the docker postgres db:

ormconfig.json
{
  "name": "default",
  "type": "postgres",
  "host": "localhost",
  "port": 5432,
  "username": "postgres",
  "password": "postgres",
  "database": "postgres",
  "synchronize": false,
  "logging": "all",
  "logger": "advanced-console",
  "entities": ["src/entity/**/*.*"],
  "cli": {
    "entitiesDir": "src/entity",
  }
}

Typescript config

tsconfig.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "src",
    "noImplicitAny": false,
    "declaration": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": true,
    "importHelpers": true,
    "lib": ["es2017", "esnext.asynciterable"],
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmit": false,
    "outDir": "./dist",
    "pretty": true,
    "resolveJsonModule": true,
    "sourceMap": true,
    "target": "es2017",
    "typeRoots": ["node_modules/@types"]
  },
  "include": ["src/**/*.ts", "src/**/*.json", ".env"],
  "exclude": ["node_modules"]
}

Dependencies and package.json

package.json
{
  "name": "code-deep-dives-tutorial-graphql",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "apollo-server": "^2.21.0",
    "apollo-server-express": "^3.5.0",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "graphql": "^15.3.0",
    "pg": "^8.7.1",
    "reflect-metadata": "^0.1.13",
    "ts-node": "^10.4.0",
    "tslib": "^2.3.1",
    "type-graphql": "^1.1.1",
    "typeorm": "^0.2.41",
    "typeorm-seeding": "^1.6.1",
    "typescript": "^4.5.4"
  }
}

Basic server setup

We use TypeGraphQL to create a schema, and then we pass that config into ApolloServer.

This is the basics of it:

src/server.ts
import {createConnection, getConnectionOptions} from "typeorm";
import "reflect-metadata";
import {ApolloServer} from "apollo-server";
import {buildSchema} from "type-graphql";

(async function () {
  const config = await getConnectionOptions();
  await createConnection(config);
  const schema = await buildSchema({ // << TypeGraphQL
    resolvers: [__dir + '/entities/**/*.ts'], 
  });
  const apolloServer = new ApolloServer({ // << Apollo
    schema, 
    playground: true, // note - this will not work on latest ApolloServer config
  });
  await apolloServer.listen({port: 6123});
  console.log("Apollo server has started on http://localhost:6123!");
})()

Creating a resolver

Simple resolver with a query

A resolver responds to the incoming GraphQL requests (queries or mutations).

They are basically like a controller if you are more used to traditional express apps or MVC apps.

A very basic example of a resolver is below. We have to add the @Resolver above the class definition, and @Query above every query.

src/resolvers/example-resolver.ts
import {Query, Resolver} from "type-graphql";

@Resolver()
export class ExampleResolver {
    @Query(() => String)
    sayHello() {
        return "hello, world!";
    }
}

You use @Query(() => String) when that function is going to return a string.

Later on you will see it being used with TypeORM entities (objects).

Simple resolver with a mutation

Let's expand on the resolver, and add a mutation.

For now I'm going to just pretend we're speaking to a database. At the moment it will just update a variable message.

src/resolvers/example-resolver.ts
import {Arg, Query, Mutation, Resolver} from "type-graphql";

let message : string | undefined = undefined; 
@Resolver()
export class ExampleResolver {
    @Query(() => String, {nullable: true})
    message() {
        if(message === undefined) return null
        return `hello, ${message}!`;
    }

    @Mutation(() => Boolean)
    updateMessage(@Arg('newMessage') newMessage: string): bool {
        message = newMessage;
        return true;
    }
}

Now if you do a mutation to updateMessage, it will update message variable (until you restart the server), and returns true.

I've also added {nullable: true} to the query message. Without this it would not be valid to return null/undefined.

Getting data from the database

Let's set up a TypeORM entity. I'm going to create it based on Twitter, so we could have tweets which are created by a user. For now I'll focus just on the tweets.

In ./src/entity let's create a 'tweet.ts` file. This is going to be using TypeORM decorators and also TypeGraphQL decorators.

src/entity/tweet.ts

import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
} from "typeorm";
import { ObjectType, Field, ID } from "type-graphql";

@ObjectType() // << type graph ql
@Entity({ name: 'tweets' }) // << type orm
export class Tweet {
    @Field( () => ID) << type graph ql
    @PrimaryGeneratedColumn() // << type orm
    id: number;

    @Field(() => String) << type graph ql
    @Column({type: "varchar"}) // << type orm
    body: string;
}

This entity has just two fields - id and body. It has decorators to tell TypeGraphQL that the id field is a ID (identifier - like a primary key), and body is a String field.

In a real app we would set up dependancy injection, but for this tutorial I'll skip that.

Import getRepository from TypeORM, and create a new mutation:

src/resolvers/example-resolver.ts
import {Arg, Mutation, Resolver} from "type-graphql";

@Resolver()
export class ExampleResolver {
    @Mutation(() => Tweet)
    async createTweet(@Arg('body') body: string): Promise<Tweet> {
       const tweetRepo = getRepository(Tweet);
       const newTweet = new Tweet();
       newTweet.body = body;
       await tweetRepo.save(newTweet);
       return newTweet;
    }
}

Now if you send a mutation request to createTweet, it'll save it in the database.

Let's now add a query to get all tweets.

src/resolvers/example-resolver.ts
import {Arg, Query, Mutation, Resolver} from "type-graphql";

@Resolver()
export class ExampleResolver {
    @Mutation(() => Tweet)
    async createTweet(@Arg('body') body: string): Promise<Tweet> {
       const tweetRepo = getRepository(Tweet);
       const newTweet = new Tweet();
       newTweet.body = body;
       await tweetRepo.save(newTweet);
       return newTweet;
    }

    @Query(() => [Tweet])
    async all(): Promise<Tweet[]> {
       const tweetRepo = getRepository(Tweet);
       return await tweetRepo.find()
    }
}

Using integers and floats in TypeGraphQL

In JS we just have the number (and BigNumber) type. They're float numbers, and we don't really have an easy way to define something as only a int or float. So in TypeGraphQL there are aliases to help with this:

example-resolver.ts
// import the aliases
import { ID, Float, Int } from "type-graphql";

@ObjectType()
class MysteryObject {
  @Field(type => ID)
  readonly id: string;

  @Field(type => Int)
  emailCount: number;

  @Field(type => Float)
  popularityScore: number;
}

We still use the typescript number typing on the class, but we tell TypeGraphQL's Field function that it is either an Int or Float.

You can also see the special ID type used, which is used to tell graph ql a field is the identifier (like a primary key).

Dates and timestamps in TypeGraphQL

There are a couple of ways to represent dates in TypeGraphQL - either as numbers (timestamp) or strings (isoDate). This can be set when you setup buildSchema() with config such as:

server.ts
import { buildSchema } from "type-graphql";

const schema = await buildSchema({
  resolvers: [ /* ... */ ],
  dateScalarMode: 'timestamp', // "timestamp" or "isoDate"
});

By default they'll come out as isoDate such as 2021-12-12T12:08:20.221Z

Custom scalar types in TypeGraphQL

Maybe you have a special object that you need to write a custom scalar type for. This is quite straightforward in TypeGraphQL

Example:

custom-scalar-type.ts
import { GraphQLScalarType, Kind } from "graphql";
import { ObjectId } from "mongodb";

export const ObjectIdScalar = new GraphQLScalarType({
  name: "ObjectId",
  description: "Mongo object id scalar type",
  serialize(value: unknown): string {
    // check the type of received value
    if (!(value instanceof ObjectId)) {
      throw new Error("ObjectIdScalar can only serialize ObjectId values");
    }
    return value.toHexString(); // value sent to the client
  },
  parseValue(value: unknown): ObjectId {
    // check the type of received value
    if (typeof value !== "string") {
      throw new Error("ObjectIdScalar can only parse string values");
    }
    return new ObjectId(value); // value from the client input variables
  },
  parseLiteral(ast): ObjectId {
    // check the type of received value
    if (ast.kind !== Kind.STRING) {
      throw new Error("ObjectIdScalar can only parse string values");
    }
    return new ObjectId(ast.value); // value from the client query
  },
});
your-object-type.ts
import { ObjectIdScalar } from "../ObjectId";

@ObjectType()
class User {
  @Field(type => ObjectIdScalar) // << use it here
  readonly id: ObjectId; // << and here

  @Field()
  name: string;
}

If you make heavy use of these, you might want to just import them in your initial server config, and then TypeGraphQL can automatically detect the field type.

server.ts
import { ObjectId } from "mongodb";
import { ObjectIdScalar } from "../ObjectId";
import { buildSchema } from "type-graphql";

const schema = await buildSchema({
  resolvers,
  scalarsMap: [{ type: ObjectId, scalar: ObjectIdScalar }],
});

Then you can use it like this:

@ObjectType()
class User {
  @Field() // magic goes here - no type annotation for custom scalar
  readonly id: ObjectId;
}

Enums

Enums are built into Typescript, example:

enums-implicit.ts
// automatic values of 0, 1, 2, 3
enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT,
}

const dir = Direction.LEFT; // 2

Or you can set the values:

enums-explicit.ts
enum Direction {
  UP = "up",
  DOWN = "down",
  LEFT = "left",
  RIGHT = "right",
}

const dir = DIRECTION.LEFT; // 'left'

We can also use enums in GraphQL (docs: https://graphql.org/learn/schema/#enumeration-types ), and TypeGraphQL supports it too.

import { registerEnumType } from "type-graphql";

enum Direction {
  UP = "UP",
  DOWN = "DOWN",
  LEFT = "LEFT",
  RIGHT = "RIGHT",
}

registerEnumType(Direction, {
  name: "Direction",
  description: "Direction of travel",
});

Then you can use it in your resolvers like this with the @Arg() decorator:

example-resolver.ts
@Resolver()
class ExampleResolver {

  @Mutation()
  move(@Arg("moveTo", type => Direction) moveTo: Direction): string {
    moveSomething(moveTo)
    return Direction[moveTo];
  }
}


function moveSomething(moveTo: Direction) {
 // ...
}

Returning multiple types in TypeGraphQL

Let's say you have a search resolver, where you can search for a string and it might return a Movie or an Actor.

For this we need to set up a union.

Object type definitions:

object-types.ts
@ObjectType()
class Movie {
  @Field()
  name: string;

  @Field()
  rating: number;
}

@ObjectType()
class Actor {
  @Field()
  name: string;

  @Field(type => Int)
  age: number;
}

And the resolver query method which can return either of those (note: I've not included the findAll method - lets just pretend we're using TypeORM with active record in those entities)

example-resolver.ts
import {Arg, Query, Resolver, createUnionType } from "type-graphql";

const SearchResultUnion = createUnionType({
  name: "SearchResult", 
  types: () => [Movie, Actor] as const, 
});

@Resolver()
class SearchResolver {
  @Query(returns => [SearchResultUnion])
  async search(@Arg("searchQuery") searchQuery: string): Promise<(typeof SearchResultUnion)[]> {
    const movies = await Movies.findAll(searchQuery);
    const actors = await Actors.findAll(searchQuery);

    return [...movies, ...actors];
  }
}

// for easier reading this is not run in Promise.all()

The following shows a more complicated setup of createUnionType:

const SearchResultUnion = createUnionType({
  name: "SearchResult",
  types: () => [Movie, Actor] as const,
  // our implementation of detecting returned object type
  resolveType: value => {
    if ("rating" in value) {
      return Movie; // we can return object type class (the one with `@ObjectType()`)
    }
    if ("age" in value) {
      return "Actor"; // or the schema name of the type as a string
    }
    return undefined;
  },
});

Then our query can look like this:

query {
  search(phrase: "Hello") {
    ... on Actor {
      name
      age
    }
    ... on Movie {
      name
      rating
    }
  }
}

Using GraphQL interfaces in TypeGraphQL

Although Typescript has interface support, they only exist at compile time. So to use graph ql interfaces we must use abstract classes.

Example:

interface.ts
@InterfaceType()
abstract class IPerson {
  @Field(type => ID)
  id: string;

  @Field()
  name: string;

  @Field(type => Int)
  age: number;
}

And then we can use it like this (the implements: IPerson is the important part here):

object.ts
@ObjectType({ implements: IPerson })
class Person implements IPerson {
  id: string;
  name: string;
  age: number;
}

GraphQL directives

GraphQL schema supports directives, which look like typescript decorators. You can set these up in TypeGraphQL with the @Directive('...') decorator.

Examples:

@Directive("@auth(requires: USER)")
@ObjectType()
class Foo {
  @Field()
  field: string;
}

@ObjectType()
class Bar {
  @Directive('@deprecated(reason: "Use newField")')
  @Field()
  field: string;
  
  @Directive("@lowercase")
  @Field()
  newField: string
}

Once they are set up there, you also need further config to register them. Example:

import { SchemaDirectiveVisitor } from "graphql-tools";

// build the schema as always
const schema = buildSchemaSync({
  resolvers: [ExampleResolver],
});

// register the used directives implementations
SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
  sample: SampleDirective,
});

How to use dependency injection in TypeGraphQL

It is easy to set up DI. I've used TypeDI in all of these examples as it tends to be the most commonly used DI library paired with TypeGraphQL.

import { buildSchema } from "type-graphql";
import { Container } from "typedi";

import { SampleResolver } from "./resolvers";

const schema = await buildSchema({
  resolvers: [SampleResolver],
  container: Container, // << 
});

Then your resolvers can use DI like this:

example-resolver.ts
import { Service } from "typedi";

@Service()
@Resolver(() => User)
export class ExampleResolver {
  constructor(
    private readonly userService: UserService,
  ) {}

  @Query(() => User, { nullable: true })
  async recipe(@Arg("userId") userId: string) {
    return this.userService.getOne(userId);
  }
}

How to add auth to TypeGraphQL

Using the built in @Authorized decorator, it is quite easy to set up authentication and authorization.

@ObjectType()
class MyObject {
  @Field()
  publicField: string;

  @Authorized()
  @Field()
  authorizedField: string;

  @Authorized("ADMIN")
  @Field()
  adminField: string;

  @Authorized(["ADMIN", "MODERATOR"])
  @Field({ nullable: true })
  hiddenField?: string;
}

As you can see the @Authorized decorator has an option param, which can be used to pass to your auth logic (in this case to pass the required role of the user).

You can also add the same logic to your resolvers:

example-resolver.ts
@Resolver()
class MyResolver {
  @Query()
  publicQuery(): MyObject {
    return {
      publicField: "Some public data",
      authorizedField: "Data for logged users only",
      adminField: "Top secret info for admin",
    };
  }

  @Authorized()
  @Query()
  authedQuery(): string {
    return "Authorized users only!";
  }

  @Authorized("ADMIN", "MODERATOR")
  @Mutation()
  adminMutation(): string {
    return "You are an admin/moderator, you can safely drop the database ;)";
  }
}

Now we need to set up the logic to allow to deny access to those protected by @Authorized. The most simple way is with a function:

server.ts
export const customAuthChecker: AuthChecker<ContextType> = (
  { root, args, context, info },
  roles,
) => {
  // add your logic to return true/false depending if the current user
  // has any of `roles`. E.g. look at session cookie, JWT token, etc.

  return true;
};

const schema = await buildSchema({
  resolvers: [MyResolver],
  authChecker: customAuthChecker, // << the important part
});

How to get access to the request (context) in ApolloServer

server.ts

import {createConnection, getConnectionOptions} from "typeorm";
import "reflect-metadata";
import {ApolloServer} from "apollo-server";
import {buildSchema} from "type-graphql";

(async function () {
  const config = await getConnectionOptions();
  await createConnection(config);
  const schema = await buildSchema({ 
    resolvers: [__dir + '/entities/**/*.ts'], 
  });
  const apolloServer = new ApolloServer({ // << Apollo
    schema, 
    playground: true, 

    // Important bit:
     context: ({ req }) => {
        const context = {
          req,
        };
        return context;
      },
  });
  await apolloServer.listen({port: 6123});
  console.log("Apollo server has started on http://localhost:6123!");
})()

Then you can access the context's req object

example-resolver.ts
@Resolver()
class ExampleResolver {
  @Query()
  showMessage(@Ctx() ctx: Context): string {
    const req = ctx.req;
    return 'hello, world';
  }
}

Adding validation

It is easy to add validation. Juse use the @InputType() along with validators from class-validator package:

object.ts
import { MaxLength, Length } from "class-validator";

@InputType()
export class RecipeInput {
  @Field()
  @MaxLength(30)
  title: string;

  @Field({ nullable: true })
  @Length(30, 255)
  description?: string;
}

Note: we do not need to add @IsString etc - as TypeGraphQL automatically checks those based on the typing. Nested inputs or arrays need special attention though (with @ValidateNested).

example-resolver.ts
@Resolver(of => Recipe)
export class RecipeResolver {
  @Mutation(returns => Recipe)
  async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise<Recipe> {
    // you can be 100% sure that the input is correct
    console.assert(recipeInput.title.length <= 30);
    console.assert(recipeInput.description.length >= 30);
    console.assert(recipeInput.description.length <= 255);
  }
}

Automatic validation in TypeGraphQL is enabled by default, but can be disabled:

const schema = await buildSchema({
  resolvers: [ExampleResolver],
  validate: false, 
});

and then enable it on specific cases:

example-resolver.ts
class ExampleResolver {
  @Mutation(() => Recipe)
  async addRecipe(@Arg("input", { validate: true }) recipeInput: RecipeInput) {
    // ...
  }
}