4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScript + express-graphql + TypeORM on Node.js ( for MySQL ) 環境を構築したった

Last updated at Posted at 2021-03-22

⚠️⚠️⚠️ Node で GraphQL サーバ実装するなら、以下の構成の方がオススメです ⚠️⚠️⚠️

はじめに

はじめまして。突然ですが、GraphQL、めちゃくちゃ良い技術です。
Rails に載った GraphQL を業務で使ってますが、フロントエンド開発がフッ軽になります。

もっと GraphQL に詳しくなりたい。でも、現在、フロントエンドエンジニアとして勤務中の私には、実務で GraphQL を触ることができたとしても せいぜい Type をちょろっと修正するくらい。

そこで、趣味で書いてる Vue.js 製 WEB アプリの API に GraphQL を採用することにしました。
導入から API として動かすところまでを勉強がてら実装したので、せっかくだし最小限の構成をご紹介します。備忘録も兼ねて。

・・・Rails に対してのモチベーションが高くない ☺️ ので、今回は express に載せてます。

実際にやってみた

TypeScript 使ってますが、サンプルコードの中では 面倒臭いので 厳密に取り扱っていない箇所があります。そーりー。

下準備

package.json を用意

package.json
{
  "name": "graphql-on-express",
  "dependencies": {
    "@types/cors": "^2.8.10",
    "@types/express": "^4.17.11",
    "@types/express-graphql": "^0.9.0",
    "@types/mysql": "^2.15.18",
    "@types/node": "^14.14.35",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "express-graphql": "^0.12.0",
    "graphql": "^15.5.0",
    "mysql": "^2.17.1",
    "typeorm": "^0.2.31",
    "typescript": "^4.2.3"
  },
  "devDependencies": {
    "ts-node": "^9.1.1",
    "tsconfig-paths": "^3.9.0"
  },
}

DB まわりのアレコレは TypeORM というライブラリに任せます。

・・・package.json で足りない項目がある場合はテキトーに埋めてください 😇

TypeScript、TypeORM のコンフィグを用意

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": false,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5",
    "lib": [
      "es2018",
      "dom"
    ],
    "moduleResolution": "node",
    "removeComments": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "strictFunctionTypes": false,
    "baseUrl": "./",
    "paths": {
      "@/*": [
        "src/*"
      ],
    },
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
  },
  "include": [
    "./src/**/*.ts"
  ]
}
ormconfig.json
{
  "type": "mysql",
  "host": "Your DB endpoint",
  "port": 3306,
  "username": "Your DB username",
  "password": "Your DB password",
  "database": "Your DB name",
  "synchronize": false,
  "logging": false,
  "entities": [
    "src/database/entity/**/*.ts"
  ],
  "migrations": [
    "src/database/migration/**/*.ts"
  ],
  "subscribers": [
    "src/database/subscriber/**/*.ts"
  ],
  "cli": {
    "entitiesDir": "src/database/entity",
    "migrationsDir": "src/database/migration",
    "subscribersDir": "src/database/subscriber"
  }
}

node modules をインストール

$ npm i

データまわりの作業

Entity を用意

データベースとプログラムとの間でマッピングされたデータが、どのようなデータ構造をとるのかを定義します。
TypeORM では、このようなデータモデルを Entity という名称で表現するようです。
一般的な MVC フレームワークにおいて、Model と呼ばれているモノにイメージは近いですが、Model と違ってロジックを持たせることはあまり想定してなさ気です。だから、あくまでも "Model" じゃなくて ただの "Entity" (実体) なのかと思いました (小並感

src/database/entity/user.ts
import { Entity, BaseEntity, Column, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn('increment')
  id!: number;

  @Column({ nullable: false })
  name!: string;
}

サンプルなので、id と name というカラムだけをもったシンプルな構造を用意します。

マイグレーションする

なんと、TypeORM はマイグレーションの機能まで提供してくれています。ありがとう。

$ npx ts-node node_modules/.bin/typeorm migration:generate -n user

上記を実行すると、src/database/migration/xxxxxxxxxxxxx-user.ts というファイルが生成されます。

続けて、以下を実行します。

$ npx ts-node node_modules/.bin/typeorm migration:run

ずらずらと SQL の実行ログが流れ・・・

(省略)

Migration userxxxxxxxxxxxxx has been executed successfully.
query: COMMIT

最後にこんなログが出力されれば成功です。

ターミナルから MySQL に直接ログインできる方は実際にテーブルを確認してみてください。

(省略)

mysql> desc user;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int          | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255) | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
2 rows in set (0.02 sec)

こんな感じになってるはず。

・・・テーブル名的には users であってほしいけど、Entity を複数形にしなきゃいけないのかな 🤔

ロジックまわりの作業

Type を定義

src/schema/fields/user/types.ts
import { GraphQLObjectType, GraphQLNonNull, GraphQLString, GraphQLInt, GraphQLInputObjectType } from 'graphql';

export const UserType = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLInt),
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
    }
  }
});

export const FetchUserInput = new GraphQLInputObjectType({
  name: 'FetchUserInput',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLInt),
    },
  }
});

export const CreateUserInput = new GraphQLInputObjectType({
  name: 'CreateUserInput',
  fields: {
    name: {
      type: new GraphQLNonNull(GraphQLString),
    },
  }
});

Schema を定義

実際に運用する際は、Entity に対して query と mutation があり、場合によっては、そこからさらにバリエーションが派生する、なんてこともあります。
ファイルを細かく分けていて、サンプルコードをみてるだけだと「冗長じゃね?」と思うかもしれませんが、上記の理由から処理が増えることを視野に入れてこうしてます。

src/schema/index.ts
import { GraphQLSchema } from "graphql";
import { queryType as query, mutationType as mutation } from "./fields";

export const schema = new GraphQLSchema({
  query,
  mutation,
});
src/schema/fields/index.ts
import { GraphQLObjectType } from 'graphql';
import { userField } from './user';

export const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    ...userField.query,
  }
})

export const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    ...userField.mutation,
  }
})
src/schema/fields/user/index.ts
import { userQuery as query } from './query';
import { userMutation as mutation } from './mutation';

export const userField = {
  query,
  mutation,
};
src/schema/fields/user/query.ts
import { GraphQLNonNull } from 'graphql';
import * as resolvers from './resolvers';
import { FetchUserInput, UserType } from './types';

const fetchUsers = {
  type: UserType,
  args: {
    input: {
      type: new GraphQLNonNull(FetchUserInput)
    }
  },
  resolve: resolvers.fetchUsers,
}

export const userQuery = {
  fetchUsers,
}
src/schema/fields/user/mutation.ts
import { GraphQLNonNull, GraphQLList } from 'graphql';
import * as resolvers from './resolvers';
import { UserType, CreateUserInput } from './types';

const createUser = {
  type: new GraphQLList(UserType),
  args: {
    input: {
      type: new GraphQLNonNull(CreateUserInput)
    }
  },
  resolve: resolvers.createUser
}

export const userMutation = {
  createUser,
}

"DB への問い合わせ" や "結果を受け取って返却する" などのコアとなる処理を用意

src/schema/fields/user/resolvers.ts
import { User } from "@/database/entity/user"
import { find, findOne, insert } from "../crud-assistant"

// e.g.
type CreateUserArgs = {
  input: {
    // any
  }
}

// e.g.
type FetchUserArgs = {
  input: {
    // any
  }
}

// e.g.
type FetchUsersArgs = {
  input: {
    // any
  }
}

export const createUser = async (args: CreateUserArgs): Promise<typeof User> => {
  return new Promise(async (resolve, reject) => {
    const insertInput = args.input
    const result = await insert<typeof User, CreateUserArgs["input"]>(User, insertInput)

    if (!result) {
      reject()
      return
    }

    const findOneInput = args.input
    const user = await findOne<typeof User, FetchUserArgs["input"]>(User, findOneInput)

    if (user) {
      resolve(user)
    } else {
      reject()
    }
  })
}

export const fetchUsers = async (args: FetchUsersArgs): Promise<Array<typeof User>> => {
  return new Promise(async (resolve, reject) => {
    const findInput = args.input
    const users = await find<typeof User, FetchUsersArgs["input"]>(User, findInput)

    if (users) {
      resolve(users)
    } else {
      reject()
    }
  })
}

CRUD の処理を共通化しておきます。
TypeORM の Repository のメソッドと同名で公開。

src/schema/fields/crud-assistant.ts
import { BaseEntity, createConnection, getRepository, InsertResult } from "typeorm"

export const insert = async<E extends typeof BaseEntity, I>(Entity: E, input: I): Promise<InsertResult | null> => {
  const connection = await createConnection()
  const repository = getRepository<E>(Entity)

  try {
    return new Promise(async (resolve, reject) => {
      const result = await repository
        .insert({
          ...input
        })
        .catch(async (e) => {
          reject()
        })

      await connection.close()

      resolve(result || null)
    })
  } catch (e) {
    await connection.close()
    return Promise.resolve(e)
  }
}

export const findOne = async<E extends typeof BaseEntity, I>(Entity: E, input: I): Promise<E | null> => {
  const connection = await createConnection()
  const repository = getRepository<E>(Entity)

  try {
    return new Promise(async (resolve, reject) => {
      const result = await repository
        .findOne({
          ...input
        })
        .catch(async (e) => {
          reject()
        })

      await connection.close()

      resolve(result || null)
    })
  } catch (e) {
    await connection.close()
    return Promise.resolve(e)
  }
}

export const find = async <E extends typeof BaseEntity, I>(Entity: E, input: I): Promise<E[] | null> => {
  const connection = await createConnection()
  const repository = getRepository<E>(Entity)

  try {
    return new Promise(async (resolve, reject) => {
      const result = await repository
        .find({
          ...input
        })
        .catch(async (e) => {
          reject()
        })

      await connection.close()

      resolve(result || null)
    })
  } catch (e) {
    await connection.close()
    return Promise.resolve(e)
  }
}

これでようやくロジックまわりのファイルが揃いました。

サーバまわりの作業

エントリポイントを用意

src/index.ts
import * as express from 'express'
import { graphqlHTTP } from 'express-graphql'
import * as cors from 'cors'
import { schema } from './schema'

const port = 4000
const app = express()
app.use(cors())
app.use(express.static('./'));
app.use('/', graphqlHTTP({
  schema,
  graphiql: true
}))

app.listen(port, () => {
  console.log(`Started server, http://localhost:${port}`)
});

エントリポイントを用意したら、

$ npx ts-node -r tsconfig-paths/register src/index.ts

を実行。

これで、http://localhost:4000 にアクセスすると Graph i QL という GUI が表示されるようになります。
API としてのリクエストには、POST を用います。

Vue アプリ側からは Vue Apollo 経由で API を call してます。その話はまたどこかでするかもしれないししないかもしれない。

おわりに

GraphQL について

冒頭でも触れましたが、フロントエンドの開発においてめちゃくちゃ便利です。
良い技術なわりに、あまり世に浸透していない気がする。もったいない 🥺

TypeORM について

便利ではありますが、提供してる型が微妙に扱いにくいと感じるところがあったり、(当たり前だけど) GraphQL 側にも型の指定が必要なので TypeORM ↔️ GraphQL 間で同じデータを指してるのに構造の定義が二重管理になっちゃったりと、小さな課題があるので要改善。暇なときに TypeORM 側のコードから GraphQL の Type 定義のコードを自動生成するようなスクリプトを書きたい。(すでに誰かが作ってるかも
まあ、使いこなせれば良きなライブラリな気はします。あと、日本語の文献が豊富ではないです。

おしまい。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?