Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8705

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

$
0
0

はじめに

はじめまして。突然ですが、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()exportclassUserextendsBaseEntity{@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';exportconstUserType=newGraphQLObjectType({name:'User',fields:{id:{type:newGraphQLNonNull(GraphQLInt),},name:{type:newGraphQLNonNull(GraphQLString),}}});exportconstFetchUserInput=newGraphQLInputObjectType({name:'FetchUserInput',fields:{id:{type:newGraphQLNonNull(GraphQLInt),},}});exportconstCreateUserInput=newGraphQLInputObjectType({name:'CreateUserInput',fields:{name:{type:newGraphQLNonNull(GraphQLString),},}});

Schema を定義

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

src/schema/index.ts
import{GraphQLSchema}from"graphql";import{queryTypeasquery,mutationTypeasmutation}from"./fields";exportconstschema=newGraphQLSchema({query,mutation,});
src/schema/fields/index.ts
import{GraphQLObjectType}from'graphql';import{userField}from'./user';exportconstqueryType=newGraphQLObjectType({name:'Query',fields:{...userField.query,}})exportconstmutationType=newGraphQLObjectType({name:'Mutation',fields:{...userField.mutation,}})
src/schema/fields/user/index.ts
import{userQueryasquery}from'./query';import{userMutationasmutation}from'./mutation';exportconstuserField={query,mutation,};
src/schema/fields/user/query.ts
import{GraphQLNonNull}from'graphql';import*asresolversfrom'./resolvers';import{FetchUserInput,UserType}from'./types';constfetchUsers={type:UserType,args:{input:{type:newGraphQLNonNull(FetchUserInput)}},resolve:resolvers.fetchUsers,}exportconstuserQuery={fetchUsers,}
src/schema/fields/user/mutation.ts
import{GraphQLNonNull,GraphQLList}from'graphql';import*asresolversfrom'./resolvers';import{UserType,CreateUserInput}from'./types';constcreateUser={type:newGraphQLList(UserType),args:{input:{type:newGraphQLNonNull(CreateUserInput)}},resolve:resolvers.createUser}exportconstuserMutation={createUser,}

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

src/schema/fields/user/resolvers.ts
import{User}from"@/database/entity/user"import{find,findOne,insert}from"../crud-assistant"// e.g.typeCreateUserArgs={input:{// any}}// e.g.typeFetchUserArgs={input:{// any}}// e.g.typeFetchUsersArgs={input:{// any}}exportconstcreateUser=async(args:CreateUserArgs):Promise<typeofUser>=>{returnnewPromise(async(resolve,reject)=>{constinsertInput=args.inputconstresult=awaitinsert<typeofUser,CreateUserArgs["input"]>(User,insertInput)if(!result){reject()return}constfindOneInput=args.inputconstuser=awaitfindOne<typeofUser,FetchUserArgs["input"]>(User,findOneInput)if(user){resolve(user)}else{reject()}})}exportconstfetchUsers=async(args:FetchUsersArgs):Promise<Array<typeofUser>>=>{returnnewPromise(async(resolve,reject)=>{constfindInput=args.inputconstusers=awaitfind<typeofUser,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"exportconstinsert=async<EextendstypeofBaseEntity,I>(Entity:E,input:I):Promise<InsertResult|null>=>{constconnection=awaitcreateConnection()constrepository=getRepository<E>(Entity)try{returnnewPromise(async(resolve,reject)=>{constresult=awaitrepository.insert({...input}).catch(async(e)=>{reject()})awaitconnection.close()resolve(result||null)})}catch(e){awaitconnection.close()returnPromise.resolve(e)}}exportconstfindOne=async<EextendstypeofBaseEntity,I>(Entity:E,input:I):Promise<E|null>=>{constconnection=awaitcreateConnection()constrepository=getRepository<E>(Entity)try{returnnewPromise(async(resolve,reject)=>{constresult=awaitrepository.findOne({...input}).catch(async(e)=>{reject()})awaitconnection.close()resolve(result||null)})}catch(e){awaitconnection.close()returnPromise.resolve(e)}}exportconstfind=async<EextendstypeofBaseEntity,I>(Entity:E,input:I):Promise<E[]|null>=>{constconnection=awaitcreateConnection()constrepository=getRepository<E>(Entity)try{returnnewPromise(async(resolve,reject)=>{constresult=awaitrepository.find({...input}).catch(async(e)=>{reject()})awaitconnection.close()resolve(result||null)})}catch(e){awaitconnection.close()returnPromise.resolve(e)}}

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

サーバまわりの作業

エントリポイントを用意

src/index.ts
import*asexpressfrom'express'import{graphqlHTTP}from'express-graphql'import*ascorsfrom'cors'import{schema}from'./schema'constport=4000constapp=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 定義のコードを自動生成するようなスクリプトを書きたい。(すでに誰かが作ってるかも
まあ、使いこなせれば良きなライブラリな気はします。あと、日本語の文献が豊富ではないです。

おしまい。


Viewing all articles
Browse latest Browse all 8705

Trending Articles