How to Build a Type-safe GraphQL API using Pothos and Kysely
Tags: graphql, node, typescript, tutorial
Published: 2022-08-29 21:08:14

## Introduction

There are two popular ways to create a GraphQL api, schema-first and code-first. If you prefer to use the schema-first approach in a TypeScript project, we can always use codegen from the api schema. However, there are a thousand and one ways to create an api using the code-first approach.

And to make the choice even more complicated, in the JavaScript community we have a lot of libraries when compared to other communities. But a pattern I've seen in the TypeScript community lately is the popularization of automatic data type inference because it brings a better development experience.

## What will we build today?

In today's article we are going to create a GraphQL api using the [Koa](https://github.com/koajs/koa) framework together with the [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga) library and [Pothos](https://github.com/hayes/pothos). In addition, we will use [Kysely](https://github.com/koskimas/kysely), which is a query builder entirely written in TypeScript.

## Getting Started

As a first step, create a project directory and navigate into it:

```sh
mkdir gql-ts-api
cd gql-ts-api
```

Next, initialize a TypeScript project:

```sh
npm init -y
npm install typescript @types/node --save-dev
```

Next, create a `tsconfig.json` file and add the following configuration to it:

```js
{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["ESNext"],
    "module": "ESNext",
    "rootDir": "src",
    "moduleResolution": "node",
    "baseUrl": ".", 
    "types": ["node"],
    "resolveJsonModule": true,
    "allowJs": true,
    "outDir": "dist",
    "removeComments": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

Now we can install the necessary dependencies:

```sh
npm install koa graphql @graphql-yoga/node @pothos/core @pothos/plugin-simple-objects kysely better-sqlite3 --save
npm install @types/koa @types/better-sqlite3 ts-standard --save-dev
```

Next, in `package.json` add the following properties:

```js
{
  "ts-standard": {
    "noDefaultIgnore": false,
    "ignore": [
      "dist"
    ],
    "project": "./tsconfig.json",
    "report": "stylish"
  }
}
```

Whenever you want to lint the project, just run the following command:

```sh
npx ts-standard --fix
```

### Create the Database Schema

With our project set up we can start by defining the schema of our finger base and for that we are going to create a table called `dog` that will contain columns such as `name` and `breed`. This way:

```js
// @/src/db/index.ts
import {
  Kysely,
  SqliteDialect,
  Generated
} from 'kysely'
import SQLite from 'better-sqlite3'

interface DogTable {
  id: Generated<number>
  name: string
  breed: string
}

interface Database {
  dog: DogTable
}

export const db = new Kysely<Database>({
  dialect: new SqliteDialect({
    database: new SQLite('dev.db')
  })
})
```

In today's article I'm not going to perform migrations with Kysely so I recommend that you create a table called `dog` along with the properties that were defined earlier.

### Creating a Schema Builder

The SchemaBuilder class is used to create types that will be stiched into a GraphQL schema. And the types that are defined in it are inferred in the resolvers, such as the context, in addition to this it also allows us to register the plugins we need.

```js
// @/src/builder.ts
import SchemaBuilder from '@pothos/core'
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'

import { db } from './db'

interface Root<T> {
  Context: T
}

export interface Context {
  db: typeof db
}

const builder = new SchemaBuilder<Root<Context>>({
  plugins: [SimpleObjectsPlugin]
})

builder.queryType({})
builder.mutationType({})

export { builder }
```

### Defining some types

We can now define some types in the schema of our GraphQL api, first we will create an object that will represent some information about the data that will be returned (which in the case of this article is shared between several resolvers):

```js
// @/src/schema/typeDefs.ts
import { builder } from '../builder'

export const DogObjectType = builder.simpleObject('CreateDogResponse', {
  fields: (t) => ({
    id: t.id(),
    name: t.string(),
    breed: t.string()
  })
})

export const DogObjectInput = builder.inputType('DogObjectInput', {
  fields: (t) => ({
    name: t.string({ required: true }),
    breed: t.string({ required: true }),
    id: t.int()
  })
})
```

In the code above we defined objects without having defined any data types, but we still have type-safety.

### Defining some fields

The next step is to add some fields to our schema, such as queries and mutations. In each of the resolvers we will get the object from the database through the context and we will perform the necessary operations.

We will also use the types that were created to define the returns of each of the resolvers as well as the arguments.

```js
// @/src/schema/resolvers.ts
import { builder } from '../builder'

import { DogObjectType, DogObjectInput } from './typeDefs'

builder.queryField('getDogs', (t) =>
  t.field({
    type: [DogObjectType],
    resolve: async (root, args, ctx) => {
      return await ctx.db.selectFrom('dog').selectAll().execute()
    }
  })
)

builder.queryField('getDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      id: t.arg.int({ required: true })
    },
    resolve: async (root, args, ctx) => {
      return await ctx.db.selectFrom('dog').selectAll().where('id', '=', args.id).executeTakeFirstOrThrow()
    }
  })
)

builder.mutationField('createDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      input: t.arg({
        type: DogObjectInput,
        required: true
      })
    },
    resolve: async (root, args, ctx) => {
      return await ctx.db.insertInto('dog').values({
        name: args.input.name,
        breed: args.input.breed
      }).returningAll().executeTakeFirstOrThrow()
    }
  })
)

builder.mutationField('updateDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      input: t.arg({
        type: DogObjectInput,
        required: true
      })
    },
    resolve: async (root, args, ctx) => {
      const data = {
        id: args.input.id as number,
        name: args.input.name,
        breed: args.input.breed
      }
      return await ctx.db.insertInto('dog').values(data)
        .onConflict((oc) => oc.column('id').doUpdateSet(data))
        .returningAll().executeTakeFirstOrThrow()
    }
  })
)

builder.mutationField('removeDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      id: t.arg.int({ required: true })
    },
    resolve: async (root, args, ctx) => {
      return await ctx.db.deleteFrom('dog').where('id', '=', args.id).returningAll().executeTakeFirstOrThrow()
    }
  })
)
```

We already have a lot of things related to the GraphQL schema defined, however we still need to compile our code-first schema into something that our GraphQL server can interpret.

```js
// @/src/schema/index.ts
import path from 'path'
import fs from 'fs'

import { printSchema, lexicographicSortSchema } from 'graphql'

import { builder } from '../builder'

import './resolvers'

export const schema = builder.toSchema({})

const schemaAsString = printSchema(lexicographicSortSchema(schema))
fs.writeFileSync(path.join(process.cwd(), './src/schema/schema.gql'), schemaAsString)
```

### Creating the GraphQL Server

Last but not least, we just need to create the api entry file that will contain the GraphQL server configuration, to which we will add the schema that was created by us and we will add the Kysely instance to the context of our api.

```js
// @/src/main.ts
import Koa from 'koa'
import { createServer } from '@graphql-yoga/node'

import { schema } from './schema'
import { Context } from './builder'
import { db } from './db'

const app = new Koa()

const graphQLServer = createServer<Koa.ParameterizedContext>({
  schema,
  context: (): Context => ({ db })
})

app.use(async (ctx) => {
  const response = await graphQLServer.handleIncomingMessage(ctx.req, ctx)
  ctx.status = response.status
  response.headers.forEach((value, key) => {
    ctx.append(key, value)
  })
  ctx.body = response.body
})

app.listen(4000, () => {
  console.log('Running a GraphQL API server at http://localhost:4000/graphql')
})
```

## Conclusion

As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave [here](https://github.com/FranciscoMendes10866/koa-yoga-pothos-kysely) the link to the github repository.