GraphQL

There are many advantages to GraphQL but predictability is one of the best.

Send a GraphQL query to your API and get exactly what you need, nothing more and nothing less. GraphQL queries always return predictable results.

Apollo is one of the most popular libraries to build a GraphQL API today. It works well with Node and React. We will be using it. We will also use TypeGraphQL to define our schema with TypeScript classes.

From now, we are building our app. So let's remove the migration until we need it. We'll keep one for reference.

sequelize.sync({ force: true }) will automatically create the required tables for you.

001_example.ts
import { QueryInterface } from "sequelize";

module.exports = {
  // change the database schema
  async up(query: QueryInterface) {
    // add migration here
  },

  // revert in case it goes wrong
  async down(query: QueryInterface) {
    // revert
  }
};

Move from models to entities

The vast majority of GraphQL queries and mutations are to interact with the database. To avoid repeating ourselves, we will define your models and GraphQL schema with the same files.

Rename models to entities to reflect the semantic of our files.

~/packages/api/src
$ mv models entities

Change sequelize.ts import path import * as models from "./entities";.

Build the GraphQL Schema

Install dependencies

$ yarn add graphql @types/graphql type-graphql apollo-server-express

Following the TypeGraphQL documentation, we have to import reflect-metadata before we use/import type-graphql or our resolvers.

Update Link.tsto add ObjectType and Field.

entities/Link.ts
import 'reflect-metadata';
import { Model, Column, Table, DataType } from "sequelize-typescript";
import { Field, ID, ObjectType } from 'type-graphql';

@ObjectType()
@Table({
  tableName: "link",
  comment: "Links saved by a user"
})
class Link extends Model<Link> {
  @Field(() => ID)
  @Column({
    primaryKey: true,
    allowNull: true,
    defaultValue: DataType.UUIDV4
  })
  id: string;

  @Field()
  @Column({
    allowNull: false
  })
  uri: string;
}

export { Link };

Create a resolver in a new folder GraphQL inrouters as well as resolvers to keep things tidy.

.
├── GraphQL
    ├── index.ts
    └── resolvers
        ├── Link.ts
        └── index.ts

For now, we're just going to return a hardcoded array of Links

import { Resolver, Query } from "type-graphql";
import { Link } from "../../../entities";

@Resolver(of => Link)
class LinkResolver {
  @Query(returns => [Link])
  links() {
    return [{ id: "123", uri: "http://test.com" }];
  }
}

export { LinkResolver };

Our new GraphQL router will build the schema

routers/GraphQL/index.ts
import "reflect-metadata";
import { ApolloServer } from "apollo-server-express";

import { Router } from "express";

import { buildSchema } from "type-graphql";
import * as resolvers from "./resolvers";

export class GraphQLRouter {
  public readonly router = Router();
  constructor() {
    this.buildSchema();
  }
  async buildSchema() {
    const schema = await buildSchema({
      resolvers: Object.values(resolvers)
    });
    const apolloServer = new ApolloServer({
      schema
    });
    apolloServer.applyMiddleware({ app: this.router, path: "/" });
  }
}

Don't forget to add it to app.ts

app.ts
export const createApp = () => {
  const app = express();
  // ...
  app.use("/graphql", new GraphQLRouter().router);
  return { app };
};

We are now able to send GraphQL queries. Let's open http://localhost:3000/graphql and see if it works.

It works!

Return data from the database

Update the containers with Link

container.ts
export const setLinkEntity = (entity: typeof Link) => Container.set(ContainerNames.linkEntity, entity)
export const getLinkEntity = (): typeof Link => Container.get(ContainerNames.linkEntity)

then set our Link entity when we declare it

entities/Link.ts
// ...
import { setLinkEntity } from "../containers";

// ...
class Link extends Model<Link> {
 // ...
}

setLinkEntity(Link);

export { Link };

and use it in our resolver

Pagination

Results should always be paginated for performance reasons. It's good practice to add it at the beginning.

We are going to create 2 helpers in routers/GrapqhQL.

The first one, PaginatedResponse.ts, generate a new class with to attributes:

  • items the data returned. In this case, it will be Link.

  • total the amount of items available to query.

  • hasMore will be true if you can request more items.

PaginatedResponse.ts
import { ClassType, ObjectType, Field, Int } from "type-graphql";

export function PaginatedResponse<TItem>(TItemClass: ClassType<TItem>) {
    // `isAbstract` decorator option is mandatory to prevent registering in schema
    @ObjectType({ isAbstract: true })
    abstract class PaginatedResponseClass {
        // here we use the runtime argument
        @Field(type => [TItemClass])
        // and here the generic type
        items: TItem[];

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

        @Field(type => Boolean)
        hasMore: Boolean;
    }
    return PaginatedResponseClass;
}

The second one, PaginatedArgs.ts, adds query arguments:

  • limit how many items to retrieve.

  • offset how many items to skip.

PaginatedArgs.ts
import { ArgsType, Field, Int } from "type-graphql";

@ArgsType()
class PaginatedArgs {
  @Field(type => Int, { nullable: true })
  offset?: number;

  @Field(type => Int, { nullable: true })
  limit?: number;
}

export { PaginatedArgs };

We have to set "declaration": false in tsconfig.json.

Let's modify the Linkresolver to use it:

resolvers/Link.ts
import { Resolver, Query, ObjectType, Args } from "type-graphql";
import { Link } from "../../../entities";
import { getLinkEntity } from "../../../containers";
import { PaginatedResponse } from "../helpers";
import { PaginatedArgs } from "../helpers/PaginatedArgs";

@ObjectType()
class PaginatedLink extends PaginatedResponse(Link) { }

@Resolver(of => PaginatedLink)
class LinkResolver {
  private readonly _LinkEntity = getLinkEntity();
  @Query(returns => PaginatedLink)
  async links(@Args() { limit = 20, offset = 0 }: PaginatedArgs): Promise<PaginatedLink> {
    const { rows: items, count: total } = await this._LinkEntity.findAndCountAll({
      limit,
      offset,
      order: [['createdAt', 'DESC']]
    });
    return { items, total, hasMore: offset + items.length <= total };
  }
}

export { LinkResolver };

Writing integration tests

We can use createTestClient from ApolloTesting.

Add the requires dependencies:

~/packages/api
$ yarn add apollo-server-core apollo-server-testing
  • Create a server

  • Build the schema with the resolver

  • Send the query

import { ApolloServerBase, gql } from "apollo-server-core";
import {
  createTestClient,
  ApolloServerTestClient
} from "apollo-server-testing";
import { buildSchema } from "type-graphql";
import { LinkResolver } from "./Link";
import { setLinkEntity } from "../../../containers";
import { Link } from "../../../entities";

describe("routers/GraphQL/Resolvers/Link", () => {
  let testClient: ApolloServerTestClient;
  beforeAll(async () => {
    // create a graphql schema with the resolver
    const schema = await buildSchema({
      resolvers: [LinkResolver]
    });
    // create a server to send requests to
    const server = new ApolloServerBase({ schema });
    // use the test client to send queries and mutations
    // more info https://www.apollographql.com/docs/apollo-server/testing/testing
    testClient = createTestClient(server);
  });
  describe("query/links", () => {
    it("should return the list of links in the database", async () => {
      // mock sequelize
      const findAndCountAll = jest.fn();
      const fakeLink = { id: "1", uri: "http://test" };
      findAndCountAll.mockResolvedValue({ rows: [fakeLink], count: 1 });
      setLinkEntity(({ findAndCountAll } as unknown) as typeof Link);

      // send the query
      const { data, errors } = await testClient.query({
        query: gql`
          {
            links {
              items {
                id
                uri
              }
              total
            }
          }
        `
      });

      // assert results
      expect(errors).toBeUndefined();
      expect(data!.links.items).toHaveLength(1);
      expect(data!.links.items[0]).toMatchObject(fakeLink);
      expect(data!.links.hasMore).toBeFalsy();
    });
  });
});

Conclusion

We have seen how to:

  • Retrieve and create rows in the database

  • Paginate the results

  • Write isolated integration tests for the resolvers

The next step is now to add user authentication and make sure only owners can access their links.

graphql branch available on GitHub.

resolvers/Link.ts
import { Resolver, Query } from "type-graphql";
import { Link } from "../../../entities";
import { getLinkEntity } from "../../../containers";

@Resolver(of => Link)
class LinkResolver {
    private readonly _LinkEntity = getLinkEntity()
    @Query(returns => [Link])
    async links(): Promise<Link[]> {
        const links = this._LinkEntity.findAll()
        return links;
    }
}

export { LinkResolver };

graphql branch available on GitHub.

Last updated

Was this helpful?