Abstruct
Apollo Server Express ( apollo-server-express - npm ) を使ったサンプルアプリを作ってみました。
以下のことを考慮しながら作ると少し躓いたので、この記事にまとめておきます。
目次
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