Pothos 및 Kysely를 사용하여 Type-safe GraphQL API를 구축하는 방법

소개



GraphQL API를 생성하는 두 가지 일반적인 방법은 스키마 우선과 코드 우선입니다. TypeScript 프로젝트에서 스키마 우선 접근 방식을 선호하는 경우 항상 api 스키마의 codegen을 사용할 수 있습니다. 그러나 코드 우선 접근 방식을 사용하여 API를 만드는 방법은 천 가지가 있습니다.

그리고 선택을 더욱 복잡하게 만들기 위해 JavaScript 커뮤니티에는 다른 커뮤니티와 비교할 때 많은 라이브러리가 있습니다. 그러나 최근에 TypeScript 커뮤니티에서 본 패턴은 더 나은 개발 경험을 제공하기 때문에 자동 데이터 유형 추론의 대중화입니다.

오늘은 무엇을 만들까요?



오늘의 기사에서는 Koa 라이브러리 및 GraphQL Yoga과 함께 Pothos 프레임워크를 사용하여 GraphQL API를 생성할 것입니다. 또한 전체가 TypeScript로 작성된 쿼리 빌더인 Kysely 을 사용합니다.

시작하기



첫 번째 단계로 프로젝트 디렉토리를 생성하고 해당 디렉토리로 이동합니다.

mkdir gql-ts-api
cd gql-ts-api


다음으로 TypeScript 프로젝트를 초기화합니다.

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


다음으로 tsconfig.json 파일을 만들고 다음 구성을 추가합니다.

{
  "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"]
}


이제 필요한 종속성을 설치할 수 있습니다.

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


다음으로 package.json에서 다음 속성을 추가합니다.

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


프로젝트를 lint하고 싶을 때마다 다음 명령을 실행하십시오.

npx ts-standard --fix


데이터베이스 스키마 생성



프로젝트가 설정되면 손가락 베이스의 스키마를 정의하는 것으로 시작할 수 있으며 이를 위해 dogname와 같은 열을 포함하는 breed라는 테이블을 만들 것입니다. 이 방법:

// @/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')
  })
})


오늘 기사에서는 Kysely로 마이그레이션을 수행하지 않을 것이므로 앞에서 정의한 속성과 함께 dog라는 테이블을 생성하는 것이 좋습니다.

스키마 빌더 생성



SchemaBuilder 클래스는 GraphQL 스키마에 포함될 유형을 생성하는 데 사용됩니다. 그리고 거기에 정의된 유형은 컨텍스트와 같은 리졸버에서 유추되며, 이 외에도 필요한 플러그인을 등록할 수 있습니다.

// @/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 }


일부 유형 정의



이제 GraphQL API의 스키마에서 일부 유형을 정의할 수 있습니다. 먼저 반환될 데이터에 대한 일부 정보를 나타내는 객체를 생성합니다(이 문서의 경우 여러 리졸버 간에 공유됨).

// @/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()
  })
})


위의 코드에서 데이터 유형을 정의하지 않고 개체를 정의했지만 여전히 유형 안전성이 있습니다.

일부 필드 정의



다음 단계는 쿼리 및 변형과 같은 일부 필드를 스키마에 추가하는 것입니다. 각 리졸버에서 우리는 컨텍스트를 통해 데이터베이스에서 개체를 가져오고 필요한 작업을 수행합니다.

또한 인수뿐만 아니라 각 해석기의 반환을 정의하기 위해 생성된 유형을 사용할 것입니다.

// @/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()
    }
  })
)


우리는 이미 GraphQL 스키마와 관련된 많은 것을 정의했지만 코드 우선 스키마를 GraphQL 서버가 해석할 수 있는 무언가로 컴파일해야 합니다.

// @/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)


GraphQL 서버 생성



마지막으로 GraphQL 서버 구성을 포함할 api 항목 파일을 생성하기만 하면 됩니다. 이 파일에 우리가 생성한 스키마를 추가하고 Kysely 인스턴스를 API 컨텍스트에 추가합니다.

// @/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')
})


결론



늘 그렇듯이 기사가 마음에 드셨기를 바라며 기존 프로젝트에 도움이 되었거나 단순히 사용해 보고 싶으셨기를 바랍니다.

기사에서 잘못된 부분을 발견했다면 댓글로 알려주시면 수정하겠습니다. 마치기 전에 이 기사의 소스 코드에 액세스하려면 github 저장소에 대한 링크here를 남겨둡니다.

좋은 웹페이지 즐겨찾기