その他

【GraphQL・TypeScript】GraphQLで作るAPIサーバ入門

その他
この記事は約9分で読めます。

バージョン情報

"devDependencies": {
    "@types/express": "^4.17.8",
    "@types/node": "^14.14.5",
    "nodemon": "^2.0.6",
    "ts-node": "^9.0.0",
    "typescript": "^4.0.5"
  }
  "dependencies": {
    "@mikro-orm/cli": "^4.2.3",
    "@mikro-orm/core": "^4.2.3",
    "@mikro-orm/migrations": "^4.2.3",
    "@mikro-orm/postgresql": "^4.2.3",
    "apollo-server-express": "^2.18.2",
    "argon2": "^0.27.0",
    "class-validator": "^0.12.2",
    "express": "^4.17.1",
    "graphql": "^15.4.0",
    "pg": "^8.4.2",
    "reflect-metadata": "^0.1.13",
    "type-graphql": "^1.1.0"
  }

はじめに

TypeScriptとGraphQLで簡単なUser登録APIを作成し、基本を学びます。

TypeScriptとMikroORMのインストール

npm init -y
yarn add -D @types/node typescript ts-node nodemon
yarn add @mikro-orm/cli @mikro-orm/core @mikro-orm/migrations @mikro-orm/postgresql pg
npx tsconfig.json

GraphQLとExpressのインストール

yarn add class-validator reflect-metadata express apollo-server-express graphql type-graphql
yarn add -D @types/express

postgreSQL DB作成

createdb {DB名}

エンティティ作成

touch User.ts

entities/User.ts

import { Entity, PrimaryKey, Property } from '@mikro-orm/core';
import { Field, ObjectType } from 'type-graphql';

@ObjectType()
@Entity()
export class User {
  @Field()
  @PrimaryKey()
  id!: number;

  @Field(() => String)
  @Property({ type: 'date' })
  createdAt = new Date();

  @Field(() => String)
  @Property({ type: 'date', onUpdate: () => new Date() })
  updatedAt = new Date();

  @Field(() => String)
  @Property({ type: 'text', unique: true })
  username!: string;

  @Property({ type: 'text' })
  password!: string;
}

デコレータ

- @ObjectType(): Classの型情報をGraphQLでも扱えるようにする
- @Entity(): エンティティ定義
- @Field(): GraphQLでクエリできるようにする(例えばパスワードとかには付けない)
- @Property({ type: ‘text’ }): プロパティ定義

MikroORMの設定

touch mikro-orm.config.ts

mikro-orm.config.ts

import { __prod__ } from './constants';
import { MikroORM } from '@mikro-orm/core';
import * as path from 'path';
import { Post } from './entities/Post';
import { User } from './entities/User';

export default {
  migrations: {
    path: path.join(__dirname, './migrations'), // マイグレーションファイルの保存先
    pattern: /^[\w-]+\d+\.[tj]s$/, // JS/TSファイルの両方に対応させる
  },
  entities: [Post, User], 使用するエンティティをここに
  dbName: {DB名},
  user: '',
  password: '',
  type: 'postgresql',
  debug: !__prod__,
} as Parameters<typeof MikroORM.init>[0];

マイグレーションファイル作成

mikro-orm.config.tsに追加したエンティティを元にマイグレーションファイル作成

npx mikro-orm migration:create

サーバのセットアップ

index.ts

import 'reflect-metadata';
import { MikroORM } from '@mikro-orm/core';
import { __prod__ } from './constants';
import mikroConfig from './mikro-orm.config';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';

const main = async () => {
  const orm = await MikroORM.init(mikroConfig);

  /* mikroConfigを元にマイグレーションする -> DB作成 */
  await orm.getMigrator().up();

  /* Expressサーバーの作成 */
  const app = express();

  /* ApolloServerの作成 */
  const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [],
      validate: false,
    }),
    /* GraphQL(Apollo)にMikroORMコンテクストを渡す */
    context: () => ({ em: orm.em }),
  });

  /* ApolloServerのミドルウェアとしてExpressを登録 */
  apolloServer.applyMiddleware({ app });

  /* サーバーの起動 */
  app.listen(4000, () => {
    console.log('--- サーバー起動: localhost:4000 --- ');
  });
};

main().catch((err) => {
  console.log(err);
});

リゾルバの作成

touch user.ts

# パスワードのハッシュ用モジュール
yarn add argon2

mutation/registerクエリでUserを登録させる場合

resolver/user.ts

import { Arg, Ctx, Field, InputType, Mutation, Resolver } from 'type-graphql';
import argon2 from 'argon2';
import { User } from 'src/entities/User';
import { MyContext } from 'src/types';

@InputType()
class UsernamePasswordInput {
  @Field()
  username: string;
  password: string;
}

@Resolver()
export class UserResolvers {
  @Mutation(() => User)
  async register(
    @Arg('options') options: UsernamePasswordInput,
    @Ctx() { em }: MyContext,
  ) {
    const hashedPassword = await argon2.hash(options.password);
    const user = em.create(User, {
      username: options.username,
      password: hashedPassword,
    });
    await em.persistAndFlush(user);
    return user;
  }
}

デコレータ

- @InputType(): クエリの引数を定義
- @Resolver(): リゾルバ定義
- @Mutation(() => User): mutationクエリ時にUser情報が返るよう定義
- @Arg(‘options’) options: UsernamePasswordInput: optionsという引数を設定/TypeScriptに型情報を渡す
- @Ctx() { em }: MyContext: MikroORMのコンテクスト(DB操作に必要)(orm.emを分割代入)

MikroORMの型情報を設定 -> @Ctx()に使用

types.ts

import { EntityManager, IDatabaseDriver, Connection } from '@mikro-orm/core';

export type MyContext = {
  em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
};

リゾルバをApolloサーバのスキーマに登録

index.ts

...
/* ApolloServerの作成 */
  const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [UserResolvers],
      validate: false,
    }),
...

PlayGroundで確認

http://localhost:4000/graphql

まとめ

  • TypeScriptの型情報がうまくGraphQLと連携できる
  • MikroORMも型が効いて使いやすい
  • デコレータ見慣れない場合は慣れるしかない

参考:

Fullstack React GraphQL TypeScript Tutorialhttps://youtu.be