import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { GraphQLError } from 'graphql';
interface MyContext {
user: UserInterface;
}
const server = new ApolloServer<MyContext>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
// get the user token from the headers
const token = req.headers.authorization || '';
// try to retrieve a user with the token
const user = getUser(token);
// optionally block the user
// we could also check user roles/permissions here
if (!user)
// throwing a `GraphQLError` here allows us to specify an HTTP status code,
// standard `Error`s will have a 500 status code by default
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
});
// add the user to the context
return { user };
},
});
console.log(`🚀 Server listening at: ${url}`);
Apollo Server에서는 contextValue라는 걸 제공하는데 헤더에 대한 정보를 추출할 수 있습니다 이걸 이용해 인증 및 인가에 대한 처리가 가능합니다
const resolvers = {
Query: {
adminData: (parent, args, context) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new AuthenticationError('You must be an admin to view this data');
}
return "This is private data only for administrators.";
}
}
};
Resolver에서도 가능합니다
const typeDefs = `#graphql
directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION
enum Role {
ADMIN
REVIEWER
USER
}
type User @auth(requires: USER) {
name: String
banned: Boolean @auth(requires: ADMIN)
canPost: Boolean @auth(requires: REVIEWER)
}
`;
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import typeDefs from './graphql/schema';
import resolvers from './graphql/resolvers';
import cors from 'cors';
import express from 'express';
import http from 'http';
import https from 'https';
import fs from 'fs';
const configurations = {
// Note: You may need sudo to run on port 443
production: { ssl: true, port: 443, hostname: 'example.com' },
development: { ssl: false, port: 4000, hostname: 'localhost' },
};
const environment = process.env.NODE_ENV || 'production';
const config = configurations[environment];
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
const app = express();
// our express server is mounted at /graphql
app.use('/graphql', cors<cors.CorsRequest>(), express.json(), expressMiddleware(server));
// Create the HTTPS or HTTP server, per configuration
let httpServer;
if (config.ssl) {
// Assumes certificates are in a .ssl folder off of the package root.
// Make sure these files are secured.
httpServer = https.createServer(
{
key: fs.readFileSync(`./ssl/${environment}/server.key`),
cert: fs.readFileSync(`./ssl/${environment}/server.crt`),
},
app,
);
} else {
httpServer = http.createServer(app);
}
await new Promise<void>((resolve) => httpServer.listen({ port: config.port }, resolve));
console.log('🚀 Server ready at', `http${config.ssl ? 's' : ''}://${config.hostname}:${config.port}/graphql`);
GRAPH OS라는 제품을 제공하는데 이걸 이용해 로깅을 따로 집계하고 통계를 내는 등에 역할을 도와줍니다
const myPlugin = {
// Fires whenever a GraphQL request is received from a client.
async requestDidStart(requestContext) {
console.log('Request started! Query:\n' + requestContext.request.query);
return {
// Fires whenever Apollo Server will parse a GraphQL
// request to create its associated document AST.
async parsingDidStart(requestContext) {
console.log('Parsing started!');
},
// Fires whenever Apollo Server will validate a
// request's document AST against your GraphQL schema.
async validationDidStart(requestContext) {
console.log('Validation started!');
},
};
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [myPlugin],
});
# This configuration file tells GraphQL Code Generator how
# to generate types based on our schema.
schema: "./schema.graphql"
generates:
# Specify where our generated types should live.
./src/__generated__/resolvers-types.ts:
plugins:
- "typescript"
- "typescript-resolvers"
config:
useIndexSignature: true
# More on this below!
contextType: "../index#MyContext"
Codegen은 Schema기반으로 그에 맞는 타입스크립트 코드나 리졸버 등을 자동적으로 만들어주는 역할을 해줍니다
type User {
id: ID!
name: String!
email: String
}
type Query {
getUser(id: ID!): User
}
export interface User {
id: string;
name: string;
email?: string;
}
export interface Query {
getUser: User;
}
위에 코드는 해당 스키마를 기준으로 interface가 나온 형태입니다
📝Mocking & 테스트 코드
// For clarity in this example we included our typeDefs and resolvers above our test,
// but in a real world situation you'd be importing these in from different files
const typeDefs = `#graphql
type Query {
hello(name: String): String!
}
`;
const resolvers = {
Query: {
hello: (_, { name }) => `Hello ${name}!`,
},
};
it('returns hello with the provided name', async () => {
const testServer = new ApolloServer({
typeDefs,
resolvers,
});
const response = await testServer.executeOperation({
query: 'query SayHelloWorld($name: String) { hello(name: $name) }',
variables: { name: 'world' },
});
// Note the use of Node's assert rather than Jest's expect; if using
// TypeScript, `assert`` will appropriately narrow the type of `body`
// and `expect` will not.
assert(response.body.kind === 'single');
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.hello).toBe('Hello world!');
});
executeOperation을 이용해 해당 쿼리에 어떤 파라미터를 줬을 때 결과값에 대한 테스트 코드를 작성할 수 있습니다
Object Type은 스키마에서 정의하는 대부분이다 API요청을 위한 API이름과 반환값 또는 Scalar들로 이루어진 새로운 타입을 정의합니다
type Author {
name: String
books: [Book] # []으로 리스트 정의
}
type Query {
books: [Book]
authors: [Author]
}
type Mutation {
addBook(title: String, author: String): Book
}
type Subscription {
postCreated: Post
}
📝Input
Object Type에 파라미터가 필요한 경우 Scalar타입으로 다 적을수도 있지만 따로 분리 후 Input Type을 만들어서 따로 관리 및 재사용할 수 있습니다
input BlogPostContent {
title: String
body: String
}
# Input 사용 예제
type Mutation {
createBlogPost(content: BlogPostContent!): Post
updateBlogPost(id: ID!, content: BlogPostContent!): Post
}
📝Enum
말 그대로 Enum Type이다
enum AllowedColor {
RED
GREEN
BLUE
}
📝Union
Union은 정의한 것만 허용한다라는 의미입니다
union SearchResult = Book | Author
type Book {
title: String!
}
type Author {
name: String!
}
type Query {
search(contains: String): [SearchResult!]
}
위에 코드를 보면 search에 대한 Query는 반환 값이 Book Type이던가 Author Type이여야합니다
근데 문제점이 생기는데 search는 어떤 타입을 반환할지 몰라 어떤 필드를 요청할지 모릅니다 이에 대한 해결법은 아래와 같습니다
방법1
query GetSearchResults {
search(contains: "Shakespeare") {
__typename
... on Book {
title
}
... on Author {
name
}
}
}
Book일 경우 title을 반환하고 Author일 경우 name을 반환합니다
방법2 (resolverType)
const resolvers = {
SearchResult: {
__resolveType(obj, contextValue, info){
// Only Author has a name field
if(obj.name){
return 'Author';
}
// Only Book has a title field
if(obj.title){
return 'Book';
}
return null; // GraphQLError is thrown
},
},
Query: {
search: () => { ... }
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server);
console.log(`🚀 Server ready at: ${url}`);
resolver에서는 이런식으로 처리합니
📝Interface
interface는 Type과 유사한데 상속받을 수 있습니다
interface Book {
title: String!
author: Author!
}
type Textbook implements Book {
title: String! # Must be present
author: Author! # Must be present
courses: [Course!]!
}
union과 동일하게 interface를 반환타입으로 가지는 경우에도 추가적인 필드에 대해서 어떤 걸 가질지 모른다 이에 대한 처리 방법은 아래와 같다
방법1
query GetBooks {
books {
__typename
title
... on Textbook {
courses {
# Only present in Textbook
name
}
}
... on ColoringBook {
colors # Only present in ColoringBook
}
}
}
방법2 (resolverType)
const resolvers = {
Book: {
__resolveType(book, contextValue, info){
// Only Textbook has a courses field
if(book.courses){
return 'Textbook';
}
// Only ColoringBook has a colors field
if(book.colors){
return 'ColoringBook';
}
return null; // GraphQLError is thrown
},
},
Query: {
books: () => { ... }
},
};
지정되지 않은 오류가 발생했습니다. Apollo Server가응답에서 오류 형식을 지정할때 다른 코드가 설정되지 않은 경우 코드 확장을 이 값으로 설정합니다.
📝커스텀 에러
import { GraphQLError } from 'graphql';
throw new GraphQLError('You are not authorized to perform this action.', {
extensions: {
code: 'FORBIDDEN',
argumentName: 'id',
},
});
📝공통 에러 처리
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServerErrorCode } from '@apollo/server/errors';
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
// Return a different error message
if (
formattedError.extensions.code ===
ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED
) {
return {
...formattedError,
message: "Your query doesn't match the schema. Try double-checking it!",
};
}
// Otherwise return the formatted error. This error can also
// be manipulated in other ways, as long as it's returned.
return formattedError;
},
});
const { url } = await startStandaloneServer(server);
console.log(`🚀 Server listening at: ${url}`);
formatError을 통해 전체 에러에 대한 핸들링이 가능합니다
📝에러 리포팅 기능
formatError: (formattedError, error) => {
if (error instanceof CustomDBError) {
// do something specific
}
},
다른 거에 대한 것도 가능
버그에 대한 내용을 가지고 리포팅 등의 기능도 제공합니다
📝Subscription (웹소켓)
type Subscription {
postCreated: Post
}
웹소켓에 대한 API 명세를 먼저 정의하고 따로 Graphql Websocket을 package 관리도구로 설치해 공식문서에 가르쳐준 대로 사용합니다
Graphql을 개발한 Meta에서 만든 Relay라는게 있지만 학습비용이 높고 React계열만 지원한다는 단점이 존재한다 그에 비해 Apollo는 유연하고 러닝커브가 높지 않다
📝리졸버
Apollo Server가 Graphql 작업 처리하는 방법입니다
요청이 들어오면 그에 따라 비즈니스 로직을 처리해 결과를 반환해주는 역할입니다
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Hardcoded data store
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];
// Schema definition
const typeDefs = `#graphql
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`;
// Resolver map
const resolvers = {
Query: {
books() {
return books;
},
},
};
// Pass schema definition and resolvers to the
// ApolloServer constructor
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Launch the server
const { url } = await startStandaloneServer(server);
console.log(`🚀 Server listening at: ${url}`);
위에 예시 코드의 경우 resolvers에서 books Query에 대한 리턴을 정의합니다 그 이후에 server에 스키마와 resolver를 등록합니다
📝리졸버 체이닝
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const libraries = [
{
branch: 'downtown',
},
{
branch: 'riverside',
},
];
// The branch field of a book indicates which library has it in stock
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
branch: 'riverside',
},
{
title: 'City of Glass',
author: 'Paul Auster',
branch: 'downtown',
},
];
// Schema definition
const typeDefs = `#graphql
# A library has a branch and books
type Library {
branch: String!
books: [Book!]
}
# A book has a title and author
type Book {
title: String!
author: Author!
}
# An author has a name
type Author {
name: String!
}
# Queries can fetch a list of libraries
type Query {
libraries: [Library]
}
`;
// Resolver map
const resolvers = {
Query: {
libraries() {
// Return our hardcoded array of libraries
return libraries;
},
},
Library: {
books(parent) {
// Filter the hardcoded array of books to only include
// books that are located at the correct branch
return books.filter((book) => book.branch === parent.branch);
},
},
Book: {
// The parent resolver (Library.books) returns an object with the
// author's name in the "author" field. Return a JSON object containing
// the name, because this field expects an object.
author(parent) {
return {
name: parent.author,
};
},
},
// Because Book.author returns an object with a "name" field,
// Apollo Server's default resolver for Author.name will work.
// We don't need to define one.
};
// Pass schema definition and resolvers to the
// ApolloServer constructor
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Launch the server
const { url } = await startStandaloneServer(server);
console.log(`🚀 Server listening at: ${url}`);
// 호출
query GetBooksByLibrary {
libraries {
books {
title
author {
name
}
}
}
}
리졸버 체이닝이란 리졸버의 결과가 다음 리졸버의 입력으로 연결되는 패턴입니다
위 예시 코드에 대해 설명하면 Query Libraries에 대한 정의를 하고 해당 리턴의 타입인 [Library]의 값 받습니다 그 이후 결과값을 받아 Library 타입 안에 속하는 books의 타입인 [Book]의 값을 정의합니다
이렇게 이어받아서 작업하는 걸 리졸버 체이닝이라고합니다
아래는 리졸버 체이닝에 쓰이는 인자값 입니다
parent
이전 리졸버의 반환값입니다.
args
이필드 에 제공된 모든GraphQL인수를포함하는 객체입니다. 예를 들어 를 실행할 때 리졸버에 전달되는 객체query{ user(id: "4") }는입니다. argsuser{ "id": "4" }
contextValue
특정작업을 실행하는 모든확인자간에 공유되는 개체입니다. 이를 사용하여 인증 정보, 데이터로더 인스턴스 및 확인자 전체에서 추적할 기타 항목을 포함하여 작업별 상태를 공유합니다.
info
필드이름, 루트에서 필드까지의 경로 등을포함하여 작업실행 상태에 대한 정보가 포함되어 있습니다 .
contextValue 예시
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
// Example resolver
adminExample: (parent, args, contextValue, info) => {
if (contextValue.authScope !== ADMIN) {
throw new GraphQLError('not admin!', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
},
},
};
interface MyContext {
// You can optionally create a TS interface to set up types
// for your contextValue
authScope?: String;
}
const server = new ApolloServer<MyContext>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
// Your async context function should async and
// return an object
context: async ({ req, res }) => ({
authScope: getScope(req.headers.authorization),
}),
});
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// A schema is a collection of type definitions (hence "typeDefs")
// that together define the "shape" of queries that are executed against
// your data.
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];
// Resolvers define how to fetch the types defined in your schema.
// This resolver retrieves books from the "books" array above.
const resolvers = {
Query: {
books: () => books,
},
};
const typeDefs = `#graphql
# Comments in GraphQL strings (such as this one) start with the hash (#) symbol.
# This "Book" type defines the queryable fields for every book in our data source.
type Book {
title: String
author: String
}
# The "Query" type is special: it lists all of the available queries that
# clients can execute, along with the return type for each. In this
# case, the "books" query returns an array of zero or more Books (defined above).
type Query {
books: [Book]
}
`;
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Passing an ApolloServer instance to the `startStandaloneServer` function:
// 1. creates an Express app
// 2. installs your ApolloServer instance as middleware
// 3. prepares your app to handle incoming requests
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server ready at: ${url}`);