DEV Community

loading...

Setting up a GraphQL server with TypeScript

kbariotis profile image Kostas Bariotis Originally published at kostasbariotis.com on ・5 min read

This article will go through setting up the basic structure of a GraphQL server with TypeScript. I will use apollo's Koa server package that I found to be very straightforward and also will set up some basic smoke tests with mocha and supertest. Let's go!

GraphQL, by providing its own definition language, it's often hard to co-exist with our high-level language of choice. But things don't have to be complicated, so let's try to keep it simple and create a maintainable structure and keep separate concerns as possible.

I will assume that you have a TypeScript project set up already in your preferred way. If not, you will find a complete example by the end of this blog post. Bear with me till then.

Kick off

Let's install some required packages first:

> npm install --save apollo-server-koa koa koa-router

> npm install --save-dev chai supertest mocha

Great, now let's start with setting up koa and the apollo-server-koa package. We will start with our apps' entrypoint, say server.ts:

// src/server.ts

import Koa from "koa";
import KoaRouter from "koa-router";
import { ApolloServer, gql } from "apollo-server-koa";

We will first import our required modules.

async function main() {
  const app = createApp();
  const port = process.env.PORT || 3100;

  app.listen(port);

  console.log(`Listening on port ${port}`);
}

Then we define a main function that will create a Koa app and start listening. We will come back to it later.

// src/server.ts

// ...

export function createApp(): Koa {
  const app = new Koa();

  const router = new KoaRouter();

  const server = new ApolloServer({
    typeDefs: gql(`
      type RootQuery

      type RootMutation

      schema {
        query: RootQuery
        mutation: RootMutation
      }
    `),
    context: ({ ctx }) => ctx,
    formatError: errorHandler,
    resolvers: {
      RootQuery: {},
      RootMutation: {},
    }
  });

  router.get("/healthz", ctxt => {
    ctxt.body = "ok";
  });

  router.post("/graphql", server.getMiddleware());
  router.get("/graphql", server.getMiddleware());

  app.use(router.routes());
  app.use(router.allowedMethods());

  return app;
}

The createApp function, as mentioned before, will construct our Koa app, along with the GraphQL schema and server. At the moment, our GraphQL schema is empty and does pretty much nothing. But notice how we were able to specify a Koa route for our GraphQL endpoint and a /healtz endpoint for our load balancer. Let's move on.

// src/server.ts

// ...

const errorHandler = err => {
  console.log("Error while running resolver", {
    error: err
  });

  // Hide all internals by default
  return new Error("Internal server error");
};

Our error handler is very basic and as is, will always respond with a 500 and a mystic error message. We will change that later on.

// src/server.ts

// ...

if (require.main === module) {
  main();
}

The final piece is an instruction to only call main if the file was called from the CLI and wasn't required from another module. That's important for our tests and we will see it right now.

Let's write some tests right away.

// src/server.test.ts

import supertest from "supertest";

import { createApp } from "./server";

const request = supertest(createApp().listen());

describe("Server", () => {
  it("should respond for health check", async () => {
    await request
      .get("/healthz")
      .expect("Content-Type", /json/)
      .expect(200);
  });
});

Here we are using createApp to create and return an instance of our Koa app and pass it to supertest. That will give us the environment we need to make requests. We start off with a basic request to /healtz and expect to have a valid 200 OK response.

Now let's add our first GraphQL query. Let's go back to where we defined our Appolo server:

// server.ts

// ...

const server = new ApolloServer({
  typeDefs: gql(`
    schema {
      query: Query
    }

    type Query {
      hello(name: String): String
    }
  `),
  resolvers: {
    Query: {
      hello: function hello(
        root: {},
        args: { name: string },
        context: {}
      ): String {
        return `Hello ${args.name}`;
      }
    }
  },
  formatError: errorHandler,
});

// ...

Our GraphQL schema is pretty basic now, we added a hello query and an appropriate resolver that will do nothing other than just returning the passed argument. Let's write a test before we move on:

// src/server.test.ts

// ...

describe("GraphQL Server", () => {
  it("should query hello", async () => {
    await request
      .post("/graphql")
      .send({
        query: `
          {
            hello(name: "Kostas")
          }
        `
      })
      .expect("Content-Type", /json/)
      .expect(200)
      .then(response => {
        expect(response.body.data.hello).to.eq("Hello Kostas");
      });
  });
});

Amazing, a very basic test for our query.

Scaling up

Now let's go back to our server definition. Notice how fast it will grow as we start to add more queries and mutations. Let's break it up. Start by creating a folder src/schema. That's where we will store our GraphQL definitions. Then a folder src/queries where we will store our GraphQL resolvers. Let's move away from the hello query.

// src/schema/hello.graphql

extend type Query {
  hello(name: String): String
}

And our query:

// src/queries/hello.ts

export default {
  Query: {
    hello: function hello(
      root: {},
      args: { name: string },
      context: {}
    ): String {
      return `Hello ${args.name}`;
    }
  }
}

And finally the server:

// src/server.ts

import { readFileSync } from 'fs';
import { join as pathJoin } from 'path';

// ...

  const server = new ApolloServer({
    typeDefs: gql(
      readFileSync(pathJoin(__dirname, "schema/hello.graphql")).toString()
    ),
    resolvers: {
      Query: helloQuery.Query
    },
    formatError: errorHandler
  });

// ...

Amazing! Now run our tests again and they should pass.

Now to avoid having to add new lines every time we add a new query or mutation, let's do something slightly more sophisticated. Let's traverse our schema folder and build our server based on that.

// src/server.ts

// ...

// Fetch all schema definition files
const schemaFiles = readdirSync(pathJoin(__dirname, "schema"))
  .filter(file => file.indexOf(".graphql") > 0);

// Concatanate them to create our schema
const schema = schemaFiles
  .map(file => readFileSync(pathJoin(__dirname, `schema/${file}`)).toString())
  .join();

// Based on these files, bring their respective query resolvers
const queryResolvers = schemaFiles
  .map(file => file.replace(".graphql", ""))
  .map(file => require(pathJoin(__dirname, `queries/${file}`)).default)
  .reduce(
    (initial, current) => ({
      ...initial,
      ...current.Query
    }),
    {}
  );

const server = new ApolloServer({
  typeDefs: gql(`
    type Query

    schema {
      query: Query
    }

    ${schema}
  `),
  resolvers: {
    Query: queryResolvers
  },
  formatError: errorHandler
});

// ...

Run our tests again, they should pass again. Try to start your server as well.

Did you know that apollo's server comes with a development GraphQL client? Start your server and head to /graphql on your browser on your servers' host and port.

More types

So far, we've been doing great. Notice the hello query how it returns a String type. That's great, but with TypeScript and GraphQL, we are able to define more complex type structures. As a matter of fact, we are definitely going to need them. So let's change the query to return a more complicated object.

// src/queries/hello.ts

interface HelloResponse {
  name: String;
  greeting: String;
}

export default {
  Query: {
    hello: function hello(root: {}, args: { name: string }): HelloResponse {
      return {
        name: args.name,
        greeting: `Hello ${args.name}`
      };
    }
  }
};
// src/schema/hello.graphql

type HelloResponse {
  name: String
  greeting: String
}

extend type Query {
  hello(name: String): HelloResponse
}

Amazing, now let's change our test as well.

// src/schema/hello.ts

// ...

  it("should query hello", async () => {
    await request
      .post("/graphql")
      .send({
        query: `
          {
            hello(name: "Kostas") {
              name
              greeting
            }
          }
        `
      })
      .expect("Content-Type", /json/)
      .expect(200)
      .then(response => {
        expect(response.body.data.hello.name).to.eq("Kostas");
        expect(response.body.data.hello.greeting).to.eq("Hello Kostas");
      });
  });

// ...

And run our tests again. Hopefully, they should pass as nothing changed.

Conclusion

That was it. I hope that you have been following along and managed to keep your tests green along the way.

You can find a complete example of the above on GitHub. Give it a pass and see what differences you have with your own or simply clone it and play around with it.

Let me know in the comments what you thought of the article and remember to share it with your colleagues.

Discussion (0)

pic
Editor guide