Как использовать TypeScript с GraphQL с помощью TypeGraphQL

В этом уроке мы объясним, что такое TypeScript и GraphQL, а также преимущества их использования. Затем покажем вам, как их можно использовать вместе с помощью TypeGrapQL и почему вам это нужно.

Что такое GraphQL?

GraphQL — еще один метод управления API. Это альтернатива Rest API, которая позволяет вам запрашивать «только те данные, которые вам нужны». Это помогает уменьшить объем данных, которые необходимо отправить клиенту с сервера.

Например, с помощью Rest API конечная точка может возвращать данные всех пользователей, хотя на этом этапе необходимы только их адрес электронной почты и номер телефона. Это называется «чрезмерной выборкой». С помощью GraphQL клиент может запрашивать такие конкретные данные.

GraphQL также поставляется с определениями типов, которые существуют в объектах схемы. GraphQL использует объекты Schema, чтобы знать, какие свойства доступны для запроса, и, по сути, тип принимаемых запросов. Он также выдает ошибку при выполнении непринятого запроса.

Однако эти определения типов ограничены объектами схемы. Они не дают вам общей статической типизации в вашем приложении. И именно поэтому TypeScript является отличным дополнением, как мы увидим в оставшейся части этой статьи.

Преимущества использования TypeScript и GraphQL

Использование TypeScript и GraphQL гарантирует наличие статической типизации во всем вашем приложении.

Без TypeScript вы все равно можете создавать типы запросов с помощью GraphQL. Но здесь есть ограничение.

Типы GraphQL существуют только в схеме GraphQL. buildSchema Для создания объекта схемы используется функция из библиотеки GraphQL :

const schema = buildSchema(`
    type Query {
        name(firstname: String!, lastname: String!): String
    }
`)

Мы создали объект схемы, и теперь нам нужен преобразователь:

const root = {
    name: variables => {
        return `My name is ${firstname} ${lastname}!`
    },
}

При выполнении запроса с неправильно типизированными переменными на игровой площадке GraphQL мы получали ошибки:

изображение-4 Площадка GraphQL показывает ошибку для неправильного типа, предоставленного для запроса

Но преобразователи не знают об определении типа в объекте схемы. Как видите, преобразователь — это обычная функция JavaScript. Это означает, что мы не получаем статической типизации в преобразователе.

Скажем, например, мы предоставляем преобразователю неправильные типы аргументов или возвращаем от преобразователя другой тип, который не ожидался схемой. Мы можем вносить ошибки в наш код, даже не зная об этом.

И именно поэтому TypeScript полезен. В TypeScript у нас есть определения типов в объекте схемы и в преобразователях, тем самым синхронизируя их обоих и делая наш код более предсказуемым.

Как использовать TypeScript и GraphQL

В этом разделе мы будем использовать TypeScript и GraphQL для создания простого API GraphQL на сервере Express.

Шаг 1. Создайте папку проекта.

Вы можете назвать ее как хотите, но graphql-ts-example в этом уроке мы будем использовать эту папку:

mkdir graphql-ts-example
cd graphql-ts-example
npm init -y

Шаг 2. Установите зависимости

Для этого урока мы будем использовать следующие зависимости:

  • graphql : библиотека JavaScript для GraphQL.
  • express : веб-фреймворк для Node, который позволяет нам создавать API и внутренний сервер.
  • express-graphql : для создания сервера GraphQL для API.
  • ts-node : для выполнения кода TypeScript в Node.
  • typescript : для компиляции кода TypeScript в JavaScript.
  • @types/express : для использования Express в TypeScript.
  • nodemon : для перезапуска сервера при внесении изменений.

В вашем терминале запустите:

npm install graphql express express-graphql
npm install -D nodemon ts-node @types/express typescript

Для тестирования нашего API мы будем использовать площадку GraphQL, предоставляемую express-graphql.

Шаг 3: Настройка наших скриптов

В package.json обновите scripts объект следующим образом:

"scripts": {
    "start": "nodemon --exec ts-node src/index.ts",
}

Также добавьте файл конфигурации для TypeScript tsconfig.json:

{
    "compilerOptions": {
        "target": "es2018",
        "module": "commonjs",
        "jsx": "preserve",
        "strict": true,
        "esModuleInterop": true,
        "lib": ["es2018", "esnext.asynciterable"]
    },
    "exclude": ["node_modules"]
}

Благодаря этому мы можем запустить наш сервер с помощью npm start.

Шаг 4. Напишите код

Мы создадим сервер Express с API GraphQL, который позволит нам извлекать пользователей, создавать пользователя и обновлять данные пользователя.

Создайте новый каталог с именем «src» и добавьте index.ts в него файл. У нас есть импорт в файле следующим образом:

import { buildSchema } from "graphql"
import express from "express"
import { graphqlHTTP } from "express-graphql"

Затем нам нужен список наших пользователей. В идеале это должно быть получено из базы данных, но мы запрограммируем это здесь:

const users = [
    { id: 1, name: "John Doe", email: "johndoe@gmail.com" },
    { id: 2, name: "Jane Doe", email: "janedoe@gmail.com" },
    { id: 3, name: "Mike Doe", email: "mikedoe@gmail.com" },
]

Далее мы строим схему GraphQL:

const schema = buildSchema(`
    input UserInput {
        email: String!
        name: String!

    }

    type User {
        id: Int!
        name: String!
        email: String!
    }

    type Mutation {
        createUser(input: UserInput): User
        updateUser(id: Int!, input: UserInput): User
    }

    type Query {
        getUser(id: String): User
        getUsers: [User]
    }
`)

Из нашей схемы мы определили:

  • пользовательский ввод с двумя обязательными свойствами, который необходим при создании пользователя
  • тип пользователя с тремя обязательными свойствами
  • мутация GraphQL, при которой мы создаем пользователей и обновляем пользователей
  • и запрос GraphQL для получения конкретного пользователя или всех пользователей.

Теперь нам нужно определить типы TypeScript для статической типизации:

type User = {
    id: number
    name: string
    email: string
}

type UserInput = Pick<User, "email" | "name">

Далее наши резольверы:

const getUser = (args: { id: number }): User | undefined =>
    users.find(u => u.id === args.id)

const getUsers = (): User[] => users

const createUser = (args: { input: UserInput }): User => {
    const user = {
        id: users.length + 1,
        ...args.input,
    }
    users.push(user)

    return user
}

const updateUser = (args: { user: User }): User => {
    const index = users.findIndex(u => u.id === args.user.id)
    const targetUser = users[index]

    if (targetUser) users[index] = args.user

    return targetUser
}

const root = {
    getUser,
    getUsers,
    createUser,
    updateUser,
}

И, наконец, наш экспресс-маршрут и сервер:

const app = express()

app.use(
    "/graphql",
    graphqlHTTP({
        schema: schema,
        rootValue: root,
        graphiql: true,
    })
)

const PORT = 8000

app.listen(PORT)

console.log(`Running a GraphQL API server at http://localhost:${PORT}/graphql`)

Учитывая то, что мы имеем выше, наши преобразователи типизируются в соответствии с определением схемы. Таким образом, наши резольверы синхронизированы. На экране localhost:4000/graphql мы видим игровую площадку GraphQL:

изображение-5

Хотя мы видим, насколько полезен TypeScript, мы также не можем отрицать трудности с написанием определений типов после создания объекта схемы.

Эта база кода небольшая, так что это может быть проще, но представьте себе что-то большое, со множеством преобразователей и необходимостью создавать определения типов для каждого из них.

Нам нужен лучший способ сделать это. Нам нужно что-то, что позволит нам создавать определения типов в одном месте в качестве основного источника истины, а затем использовать их в наших преобразователях и объектах схемы.

Как использовать TypeGraphQL для улучшения типизированного GraphQL

Цель TypeGraphQL — сделать удобным использование статической типизации в преобразователях и создание схем из одного места.

Он поставляется со своим синтаксисом, который представляет собой еще один процесс обучения. Но это не так уж и круто – это шаг в правильном направлении.

Давайте улучшим нашу кодовую базу с помощью TypeGraphQL.

Нам понадобится пара зависимостей:

В вашем терминале запустите:

npm install class-validator type-graphql reflect-metadata

В свой tsconfig.json добавьте к объекту следующее compilerOptions:

"compilerOptions": {
    // ...
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
}

Это важно, чтобы TypeScript не жаловался на использование декораторов. Они пока находятся в экспериментальном режиме.

Теперь давайте обновим нашу кодовую базу с помощью TypeGraphQL. Создайте новый каталог под названием «Пользователи». В нем у нас будут схема и преобразователи.

Создайте новый файл в «users» под названием «users.schema.ts»:

// users.schema.ts

import { Field, ObjectType, InputType } from "type-graphql"

@ObjectType()
export class User {
    @Field()
    id!: number
    @Field()
    name!: string
    @Field()
    email!: string
}

@InputType()
export class UserInput implements Pick<User, "name" | "email"> {
    @Field()
    name!: string
    @Field()
    email!: string
}

Во-первых, у нас есть User класс, украшенный декоратором ObjectType. Это сообщает GraphQL, что этот класс является типом GraphQL. В GraphQL это интерпретируется как:

buildSchema(`
    type User {
        id: Int!
        name: String!
        email: String!
    }

    input UserInput {
        name: String!
        email: String!
    }
`)

Далее наши резольверы. Создайте users.resolvers.ts файл в каталоге «users»:

// users.resolvers.ts

import { Query, Resolver, Mutation, Arg } from "type-graphql"
import { UserInput, User } from "./users.schema"

@Resolver(() => User)
export class UsersResolver {
    private users: User[] = [
        { id: 1, name: "John Doe", email: "johndoe@gmail.com" },
        { id: 2, name: "Jane Doe", email: "janedoe@gmail.com" },
        { id: 3, name: "Mike Doe", email: "mikedoe@gmail.com" },
    ]

    @Query(() => [User])
    async getUsers(): Promise<User[]> {
        return this.users
    }

    @Query(() => User)
    async getUser(@Arg("id") id: number): Promise<User | undefined> {
        const user = this.users.find(u => u.id === id)
        return user
    }

    @Mutation(() => User)
    async createUser(@Arg("input") input: UserInput): Promise<User> {
        const user = {
            id: this.users.length + 1,
            ...input,
        }
        
        this.users.push(user)
        return user
    }

    @Mutation(() => User)
    async updateUser(
        @Arg("id") id: number,
        @Arg("input") input: UserInput
    ): Promise<User> {
        const user = this.users.find(u => u.id === id)
        
        if (!user) {
            throw new Error("User not found")
        }

        const updatedUser = {
            ...user,
            ...input,
        }

        this.users = this.users.map(u => (u.id === id ? updatedUser : u))

        return updatedUser
    }
}

Здесь стоит обратить внимание на несколько декораторов:

  • есть Resolver декоратор, который украшает класс как объект с помощью множества методов разрешения запросов и мутаций. Прелесть здесь в том, что мы определяем запросы, мутации и методы разрешения в одном классе.
  • есть Query декоратор, который сообщает GraphQL, что это запрос, и соответствующий метод разрешения.
  • есть Mutation декоратор, который сообщает GraphQL, что это мутация и соответствующий метод разрешения
  • есть Arg декоратор, который сообщает GraphQL, что этот аргумент является аргументом GraphQL для преобразователя.

Как вы заметили, не создавая определения типа объекта User, мы могли бы просто использовать класс, экспортированный из файла схемы.

Приведенный выше код будет интерпретирован GraphQL как:

buildSchema(`
    type Query {
        getUsers: [User]
        getUser(id: Int!): User
    }

    type Mutation {
        createUser(input: UserInput): User
        updateUser(id: Int!, input: UserInput): User
    }
`)

// resolvers

Вернёмся к src/index.ts следующему коду:

import "reflect-metadata"
import { buildSchema } from "type-graphql"
import express from "express"
import { graphqlHTTP } from "express-graphql"

import { UsersResolver } from "./users/users.resolver"

async function main() {
    const schema = await buildSchema({
        resolvers: [UsersResolver],
        emitSchemaFile: true,
    })

    const app = express()

    app.use(
        "/graphql",
        graphqlHTTP({
            schema: schema,
            graphiql: true,
        })
    )

    app.listen(8000)

    console.log("Running a GraphQL API server at http://localhost:8000/graphql")
}

main()

На этот раз функция buildSchema взята из библиотеки. type-graphql Вернувшись на игровую площадку GraphQL, наши запросы работают как положено:

изображение-6

Заключение

В этой статье мы узнали, что такое GraphQL и TypeScript, и увидели ограничения использования GraphQL без TypeScript. Мы также увидели прекрасный способ совместного использования GraphQL и TypeScript — TypeGraphQL.