Abstruct

Apollo Server Express ( apollo-server-express - npm ) を使ったサンプルアプリを作ってみました。

以下のことを考慮しながら作ると少し躓いたので、この記事にまとめておきます。

  • チュートリアル Get started with Apollo Server - Apollo GraphQL Docs そのままに利用するだけだと、GraphQLのschema定義やresolverを1ファイルに記述することになり少し煩雑になってしまうので、きれいに分けたい。
  • どうせならスキーマ定義と一緒にバリデーション定義も盛り込みたい。

目次


1. Folder structure

今回、紹介するコードのフォルダ構成を載せます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
src
├── domain # 業務ロジック
│   ├── model
│   │   ├── Sample1Result.ts
│   │   └── Sample2Result.ts
│   ├── sample1
│   │   └── sample1.ts
│   ├── sample2
│   │   └── sample2.ts
├── interface # Apollo Serverと業務ロジックの関連付け
│   ├── errorHandler.ts
│   ├── index.ts
│   ├── sample1
│   │   └── sample1Resolver.ts
│   └── sample2
│       └── sample2Resolver.ts
├── server.ts

2. Source Code

以下、単にソースコードを貼り付けておきます。
大事なポイントはインラインコメントで記載しておきます。

2.1. server.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { ApolloServer } from 'apollo-server-express'
import {
  ApolloServerPluginInlineTraceDisabled,
  ApolloServerPluginLandingPageProductionDefault,
} from 'apollo-server-core'
import express from 'express'
import http from 'http'
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core/dist/plugin/drainHttpServer'
import { formatError } from '@/interface/errorHandler'
import { schema } from '@/interface'

async function listen(port: number): Promise<void> {
  const app = express()
  const httpServer = http.createServer(app)

  const server = new ApolloServer({
    schema, // ★ポイント1 スキーマの設定
    formatError, // ★ポイント2 エラーハンドラーの設定
    plugins: [
      ApolloServerPluginLandingPageProductionDefault({ footer: false }),
      ApolloServerPluginDrainHttpServer({ httpServer }),
      ApolloServerPluginInlineTraceDisabled(),
    ],
  })
  await server.start()
  server.applyMiddleware({ app })

  return new Promise((resolve, reject): void => {
    httpServer.listen(port).once('listening', resolve).once('error', reject)
  })
}

listen(4000).then((): void => {
  console.info('🚀 Server is ready at localhost:4000')
})

2.2. interface/index.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {
  checkAuthSettingsDef,
  sample1Resolver,
} from '@/interface/sample1/sample1Resolver'
import { sample2Def, sample2Resolver } from '@/interface/sample2/sample2Resolver'
import { makeExecutableSchema, mergeSchemas } from '@graphql-tools/schema'
import { constraintDirective, constraintDirectiveTypeDefs } from 'graphql-constraint-directive'

export const schema = mergeSchemas({
  schemas: [
    constraintDirective()(
      // ポイント1 スキーマ定義とリゾルバーの関連付け
      // ポイント2 constraintDirectiveTypeDefs によって、graphql-constraint-directive で設定したバリデーションルールが適用される。
      makeExecutableSchema({
        resolvers: sample1Resolver(),
        typeDefs: [constraintDirectiveTypeDefs, checkAuthSettingsDef],
      })
    ),
    constraintDirective()(
      makeExecutableSchema({
        resolvers: sample2Resolver(),
        typeDefs: [constraintDirectiveTypeDefs, sample2Def],
      })
    ),
  ],
})

2.3. interface/errorHandler.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { ValidationError } from 'apollo-server-core'
import { GraphQLError, GraphQLFormattedError } from 'graphql'

export const formatError = (error: GraphQLError): GraphQLFormattedError => {
  const code = error?.extensions.code
  if (code === 'GRAPHQL_VALIDATION_FAILED') {
    // return a custom object
    return new ValidationError(error.message)
  }
  return error
}

2.4. interface/sample1/sample1Resolver.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { sample1 } from '@/domain/sample1/sample1'
import { GraphQLResolverMap } from '@apollographql/apollo-tools'
import { Sample1Result } from '@/domain/model/Sample1Result'

export const checkAuthSettingsDef = `
  """
  サンプル1結果
  """
  type Sample1Result {
    """
    結果
    """
    result: Boolean
  }

  """
  サンプル1
  """
  type Query {
    """
    サンプル1
    """
    sample1(input: Sample1Input!): Sample1Result!
  }
  """
  サンプル1インプット
  """
  input Sample1Input {
    """
    ID
    """
    id: String! @constraint(minLength: 5)
  }
`
// ポイント1 ↑のschema定義にて、`@constraint(minLength: 5)` というバリデーションルールを定義している。

export const sample1Resolver = (): GraphQLResolverMap<object> => {
  return {
    Query: {
      sample1: (_parent: object, args: { id: string }): Sample1Result => {
        return sample1(args.id)
      },
    },
  }
}

2.5. domain/sample1/sample1.ts

1
2
3
4
5
6
import { Sample1Result } from '@/domain/model/Sample1Result'

export function sample1(id: string): Sample1Result {
  console.info('sample1: ' + id)
  return { result: true }
}

2.6. domain/model/Sample1Result.ts

1
2
3
export interface Sample1Result {
  result: boolean
}

2.7. interface/sample2/sample2Resolver.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { sample2 } from '@/domain/sample2/sample2'
import { GraphQLResolverMap } from '@apollographql/apollo-tools'
import { Sample2Result } from '@/domain/model/Sample2Result'

export const sample2Def = `
  """
  サンプル2結果
  """
  type Sample2Result {
    """
    結果
    """
    result: Boolean
  }
  """
  サンプル2
  """
  type Mutation {
    """
    サンプル2
    """
    sample2(input: Sample2Input!): Sample2Result!
  }
  """
  サンプル2インプット
  """
  input IdCheckInput {
    """
    ID
    """
    id: String! @constraint(minLength: 5)
  }
`

export const sample2Resolver = (): GraphQLResolverMap<object> => {
  return {
    Mutation: {
      sample2: (_parent: object, args: { id: string }): Sample2Result => {
        return sample2(args.id)
      },
    },
  }
}

2.8. domain/sample2/sample2.ts

1
2
3
4
5
6
7
import { Sample2Result } from '@/domain/model/Sample2Result'

// ID存在チェック
export function sample2(id: string): Sample2Result {
  console.info('sample2: ' + id)
  return { result: true }
}

2.9. domain/model/Sample2Result.ts

1
2
3
export interface Sample2Result {
  result: boolean
}

3. Reference