Chapter 2: TypeGraphQL

Full Stack TypeScript

link TypeGraphQL

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

  • Create database entities using TypeORM
  • Create queries and resolvers with TypeGraphQL
  • Respond to a query with data from a resolver

Introduction

In this section, we begin to create a GraphQL backend using TypeGraphQL. With TypeGraphQL, we can utilize TypeScript and GraphQL to create static types and queries for our database. Once we complete this setup, we will generate queries using GraphQL Code Generator.

To generate and manage our database models we use TypeORM. TypeORM is an Object Relational Mapping for TypeScript, and is supported by the TypeGraphQL libray.

link Overview

Before getting started, take a tour of these libraries:

Let's get started!

We shall begin building our TypeGraphQL server by installing the TypeORM CLI, and running the init command:

npx typeorm init --name travel-api --database postgresql
cd travel-api
yarn

link TypeGraphQL

Then, switch into your new directory and install these dependencies:

yarn add apollo-server-express class-transformer connect-sqlite3 express express-session graphql graphql-toolkit pg sqlite3 type-graphql 

Adding pg and apollo-server-express will allow us to develop an Apollo Server built on top of Express with a PostgresQL database. We utlize sqlite3 for local development.

Let's also add the necessary dev dependencies for Express.

yarn add -D ts-node-dev nodemon @types/express-session @types/express

link TypeORM Config

For local development we are going to use sqlite3. This means our database will be inside of a file called database.sqlite.

Rename ormconfig.json, to ormconfig.js, and populate it with the following:

module.exports = [
   {
     name: "development",
     type: "sqlite",
     database: "database.sqlite",
     synchronize: true,
     logging: true,
     entities: ["src/entity/**/*.ts"],
     migrations: ["src/migration/**/*.ts"],
     subscribers: ["src/subscriber/**/*.ts"],
     cli: {
       entitiesDir: "src/entity",
       migrationsDir: "src/migration",
       subscribersDir: "src/subscriber"
     }
   },
   {
     name: "production",
     type: "postgres",
     url: process.env.DATABASE_URL,
     synchronize: false, // switch this to false once you have the initial tables created and use migrations instead
     logging: false,
     entities: ["dist/entity/**/*.js"],
     migrations: ["dist/migration/**/*.js"],
     subscribers: ["dist/subscriber/**/*.js"],
     cli: {
       entitiesDir: "dist/entity",
       migrationsDir: "dist/migration",
       subscribersDir: "dist/subscriber"
     }
   }
 ];

link TypeScript Config

We configure TypeScript with custom compiler options. Let's modify tsconfig.json with the following:

{
  "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", "./src/generated"],
  "include": ["./src/**/*.tsx", "./src/**/*.ts"]
}

link Simple Server

We are getting started with the TypeScript server functionality, so now would be a great time to test things out. Let's modify our src/index.ts with an async function called bootstrap.

  • Note that we imported getConnectionOptions and created dbOptions using the default option.
import 'reflect-metadata';
import { createConnection, getConnectionOptions } from 'typeorm';
import { User } from './entity/User';

async function bootstrap() {
  // get options from ormconfig.js
  const dbOptions = await getConnectionOptions(
    process.env.NODE_ENV || 'development'
  );
  createConnection({ ...dbOptions, name: 'default' })
    .then(async connection => {
      console.log('Inserting a new user into the database...');
      const user = new User();
      user.firstName = 'Timber';
      user.lastName = 'Saw';
      user.age = 25;
      await connection.manager.save(user);
      console.log('Saved a new user with id: ' + user.id);

      console.log('Loading users from the database...');
      const users = await connection.manager.find(User);
      console.log('Loaded users: ', users);

      console.log(
        'Here you can setup and run express/koa/any other framework.'
      );
    })
    .catch(error => console.log(error));
}
bootstrap();

Each time the server boots up, one new user is added and we see a full list of user. Try changing the new user's first name, last name, or age.

We also see the raw SQL queries executed on launch. This is because we have an option called logging set to true in our ormconfig.js.

TypeORM is handling the databse setup, and performing migrations on the fly upon startup. We can see the results for each User entity.

link Place Entity

Entities allow us to create and update database models. We can now try adding our first model: Place.

Create a new file called src/entity/Place.ts and insert the following:

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

@ObjectType({ description: 'Destination or place of interest' })
@Entity()
export class Place extends BaseEntity {
  @Field(() => ID)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column()
  title: string;

  @Field({
    nullable: true,
    description: 'The place description'
  })
  @Column()
  description?: string;

  @Field({
    nullable: true,
    description: 'The place image url'
  })
  @Column()
  imageUrl?: string;

  @Field({ nullable: true })
  @Column()
  creationDate?: Date;
}

Display Entity

If we wanted to test out our new entity, we could try to generate a few sample places for the database.

import { Place } from './entity/Place';

{/* ... */}

createConnection({ ...dbOptions, name: 'default' })
    .then(async connection => {
      console.log('Inserting a new place into the database...');
      const place = new Place();
      place.title = 'New York';
      place.description = 'The Big Apple';
      place.imageUrl = 'https://images.unsplash.com/photo-1485738422979-f5c462d49f74'
      place.creationDate = new Date();
      await connection.manager.save(place);
      console.log('Saved a new place with id: ' + place.id);

      console.log('Loading places from the database...');
      const places = await connection.manager.find(Place);
      console.log('Loaded places: ', places);

      console.log(
        'Here you can setup and run express/koa/any other framework.'
      );
    })
    .catch(error => console.log(error));

link Place Input

In order to define and validate our TypeGraphQL resolver, we create a new file called graphql-types/PlaceInput.ts. This file contains any required parameters for our first TypeGraphQL entity, Place.ts.

mkdir src/graphql-types
touch src/graphql-types/PlaceInput.ts

Let's modify graphql-types/PlaceInput.ts to provide the arguments for adding a new place.

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

@InputType()
export class PlaceInput {
  @Field({ nullable: true })
  id?: number;

  @Field()
  title: string;

  @Field({ nullable: true })
  description?: string;

  @Field({ nullable: true })
  imageUrl?: string;
}

link Place Resolver

In order to server responses from a TypeGraphQL server, we will set up our first TypeGraphQL resolver. Resolvers serve data related to entities, so we can create a new file called resolvers/PlaceResolver.ts.

mkdir src/resolvers
touch src/resolvers/PlaceResolver.ts

Let's modify resolvers/PlaceResolver.ts to provide queries and mutations for creating and fetching new places:

import { Resolver, Query, Arg, Mutation } from 'type-graphql';
import { plainToClass } from 'class-transformer';

import { Place } from '../entity/Place';
import { PlaceInput } from '../graphql-types/PlaceInput';

@Resolver(() => Place)
export class PlaceResolver {
  @Query(() => Place, { nullable: true })
  async place(@Arg('id') id: number): Promise<Place | undefined> {
    return await Place.findOne(id);
  }

  @Query(() => [Place], {
    description: 'Get all the places from around the world '
  })
  async places(): Promise<Place[]> {
    const places = await Place.find();
    return places;
  }

  @Mutation(() => Place)
  async createPlace(@Arg('place') placeInput: PlaceInput): Promise<Place> {
    const place = plainToClass(Place, {
      description: placeInput.description,
      title: placeInput.title,
      imageUrl: placeInput.imageUrl,
      creationDate: new Date()
    });
    const newPlace = await Place.create({ ...place }).save();
    return newPlace;
  }
}

@Query and @Mutation are useful decorators for our first @Resolver class. A decorator assumes certain properties on a given class or function. So in this case, we add decorators to our resolver's three functions:

  • async place(@Arg('id') id: number): Promise<Place | undefined>: Returns a single place based on it's ID, or undefined.

  • async places(): Promise<Place[]>: Returns a list of all places, or an empty array.

  • async createPlace(@Arg('place') placeInput: PlaceInput): Promise<Place>: Adds and returns a new place using PlaceInput parameters.

link Apollo Server

Now that we defined our first TypeGraphQL resolver, we are going to initialize a new ApolloServer. Let's modify the src/index.ts to include the apollo server.

Starting at the top level imports, add the following modules:

import 'reflect-metadata';
import { createConnection, getConnectionOptions } from 'typeorm';
import express from 'express';
import session from 'express-session';
import connectSqlite3 from 'connect-sqlite3';
import { ApolloServer } from 'apollo-server-express';
import * as path from 'path';
import { buildSchema } from 'type-graphql';

import { PlaceResolver } from './resolvers/PlaceResolver';

const SQLiteStore = connectSqlite3(session);

You are going to see an issue related to connect-sqlite3, this is acceptable - we will resolve it soon.

Up next, add an express instance and connect it to your sqlite3 database:

async function bootstrap() {
  const app = express();
  // use express session
  app.use(
    session({
      store: new SQLiteStore({
        db: 'database.sqlite',
        concurrentDB: true
      }),
      name: 'qid',
      secret: process.env.SESSION_SECRET || 'aslkdfjoiq12312',
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        maxAge: 1000 * 60 * 60 * 24 * 7 * 365 // 7 years
      }
    })
  );

  // get options from ormconfig.js
  const dbOptions = await getConnectionOptions(
    process.env.NODE_ENV || 'development'
  );
  createConnection({ ...dbOptions, name: 'default' })
    .then(async () => {
      /* Initialize apollo server here */
    })
    .catch(error => console.log(error));
}
bootstrap();

Now that we initialized a server connection, we can create the ApolloServer instance. Modify the callback function of createConnection with the following:

/* Initialize apollo server here */
// build TypeGraphQL executable schema
const schema = await buildSchema({
  // add all typescript resolvers
  // __dirname + '/resolvers/*.ts'
  resolvers: [PlaceResolver],
  validate: true,
  // automatically create `schema.gql` file with schema definition in current folder
  emitSchemaFile: path.resolve(__dirname, 'schema.gql')
});

// Create GraphQL server
const apolloServer = new ApolloServer({
  schema,
  context: ({ req, res }) => ({ req, res }),
  introspection: true,
  // enable GraphQL Playground
  playground: true
});

apolloServer.applyMiddleware({ app, cors: true });

const port = process.env.PORT || 4000;
// Start the server
app.listen(port, () => {
  console.log(`Server started at http://localhost:${port}/graphql`);
});

Inside the callback function, we build our server schema and then initialize a new ApolloServer instance. Once the server starts we allow cors and run it on an associated port.

GraphQL Schema

Our GraphQL schema defines two unique properties: typeDefs and resolvers. Let's dig into their details:

  • typeDefs define what data types are accessible in the server. In this case, we create unique Entity models that act as our GraphQL type definitions.

  • resolvers will execute logic based on the TypeORM entity models. In this case, we used an @Resolver decorator to associate resolvers with queries and mutations.

There's only one small issue...

link Custom Types

Sometimes your project modules include an unsupported type. In this case we have a module named connect-sqlite3. Let's add a custom module declaration to begin running the server.

mkdir src/@types
touch src/@types/connect-sqlite3.d.ts

Inside your new src/@types/connect-sqlite3.d.ts insert the following snippet:

declare module 'connect-sqlite3';

This makes the TypeScript compiler happy, and now we can run the server!

Test Server

Modify your package.json file to support ts-node-dev.

"scripts": {
  "start": "ts-node src/index.ts",
  "dev": "ts-node-dev --respawn src/index.ts"
},

Let's make sure our server works:

yarn start

Open http://localhost:4000/graphql, you should see a playground for your GraphQL endpoints.

Insert the following query to see your list of places:

query {
  places {
    id
    title
    description
    imageUrl
  }
}

If you see the list of places, well done!

Now let's try a new place mutation:

mutation CreatePlace {
  createPlace(place: {
    title: "Central Park"
    description: "The Great Lawn",
    imageUrl: "https://images.unsplash.com/photo-1568515387631-8b650bbcdb90"
  }) {
    title
    description
    imageUrl
    creationDate
  }
}

And try fetching a specific place:

query GetPlace {
  place(id: 1) {
    title
    description
    imageUrl
    creationDate
  }
}

We now have an API with some CRUD functionality built in.

Well Done!

Wrapping Up

Today we made a TypeGraphQL server which displays and creates places. The next chapter introduces GraphQL Codegen, to automate our frontend network requests!

format_list_bulleted
help_outline