apollo-server-express
Version:
Production-ready Node.js GraphQL server for Express
226 lines (202 loc) • 7.69 kB
text/typescript
import express from 'express';
import corsMiddleware from 'cors';
import { json, OptionsJson } from 'body-parser';
import {
GraphQLOptions,
ApolloServerBase,
Config,
runHttpQuery,
convertNodeHttpToRequest,
isHttpQueryError,
} from 'apollo-server-core';
import accepts from 'accepts';
export { GraphQLOptions } from 'apollo-server-core';
export interface GetMiddlewareOptions {
path?: string;
cors?:
| corsMiddleware.CorsOptions
| corsMiddleware.CorsOptionsDelegate
| boolean;
bodyParserConfig?: OptionsJson | boolean;
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
// There's no real point to allowing you to customize the health check path in
// an Apollo Server web framework integration package. You're already using
// Express --- just define a health check yourself by adding a handler that
// returns 200 to the URL path of your choice. This option only exists to
// provide a small amount of configuration for `apollo-server`, which doesn't
// otherwise give you direct access to the web server. (Honestly, the health
// check feature really should *only* exist in `apollo-server`; that it exists
// elsewhere (and doesn't even check to see if GraphQL operations can
// execute!) is a mistake we're stuck with due to backwards compatibility.)
// Passing `null` here implies disableHealthCheck:true.
__internal_healthCheckPath?: string | null;
}
export interface ServerRegistration extends GetMiddlewareOptions {
// Note: You can also pass a connect.Server here. If we changed this field to
// `express.Application | connect.Server`, it would be very hard to get the
// app.use calls to typecheck even though they do work properly. Our
// assumption is that very few people use connect with TypeScript (and in fact
// we suspect the only connect users left writing GraphQL apps are Meteor
// users).
app: express.Application;
}
export interface ExpressContext {
req: express.Request;
res: express.Response;
}
export type ApolloServerExpressConfig = Config<ExpressContext>;
export class ApolloServer<
ContextFunctionParams = ExpressContext,
> extends ApolloServerBase<ContextFunctionParams> {
// This translates the arguments from the middleware into graphQL options It
// provides typings for the integration specific behavior, ideally this would
// be propagated with a generic to the super class
async createGraphQLServerOptions(
req: express.Request,
res: express.Response,
): Promise<GraphQLOptions> {
const contextParams: ExpressContext = { req, res };
return super.graphQLServerOptions(contextParams);
}
public applyMiddleware({ app, ...rest }: ServerRegistration) {
// getMiddleware calls this too, but we want the right method name in the error
this.assertStarted('applyMiddleware');
app.use(this.getMiddleware(rest));
}
// TODO: While `express` is not Promise-aware, this should become `async` in
// a major release in order to align the API with other integrations (e.g.
// Hapi) which must be `async`.
public getMiddleware({
path,
cors,
bodyParserConfig,
disableHealthCheck,
onHealthCheck,
__internal_healthCheckPath,
}: GetMiddlewareOptions = {}): express.Router {
if (!path) path = '/graphql';
this.assertStarted('getMiddleware');
// Note that even though we use Express's router here, we still manage to be
// Connect-compatible because express.Router just implements the same
// middleware interface that Connect and Express share!
const router = express.Router();
if (!disableHealthCheck && __internal_healthCheckPath !== null) {
router.use(
__internal_healthCheckPath ?? '/.well-known/apollo/server-health',
(req, res) => {
// Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');
if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
res.json({ status: 'pass' });
})
.catch(() => {
res.status(503).json({ status: 'fail' });
});
} else {
res.json({ status: 'pass' });
}
},
);
}
// XXX multiple paths?
this.graphqlPath = path;
// Note that we don't just pass all of these handlers to a single app.use call
// for 'connect' compatibility.
if (cors === true) {
router.use(path, corsMiddleware<corsMiddleware.CorsRequest>());
} else if (cors !== false) {
router.use(path, corsMiddleware(cors));
}
if (bodyParserConfig === true) {
router.use(path, json());
} else if (bodyParserConfig !== false) {
router.use(path, json(bodyParserConfig));
}
const landingPage = this.getLandingPage();
router.use(path, (req, res, next) => {
if (landingPage && prefersHtml(req)) {
res.setHeader('Content-Type', 'text/html');
res.write(landingPage.html);
res.end();
return;
}
if (!req.body) {
// The json body-parser *always* sets req.body to {} if it's unset (even
// if the Content-Type doesn't match), so if it isn't set, you probably
// forgot to set up body-parser.
res.status(500);
if (bodyParserConfig === false) {
res.send(
'`res.body` is not set; you passed `bodyParserConfig: false`, ' +
'but you still need to use `body-parser` middleware yourself.',
);
} else {
res.send(
'`res.body` is not set even though Apollo Server installed ' +
"`body-parser` middleware; this shouldn't happen!",
);
}
return;
}
runHttpQuery(
[],
{
method: req.method,
options: () => this.createGraphQLServerOptions(req, res),
query: req.method === 'POST' ? req.body : req.query,
request: convertNodeHttpToRequest(req),
},
this.csrfPreventionRequestHeaders,
).then(
({ graphqlResponse, responseInit }) => {
if (responseInit.headers) {
for (const [name, value] of Object.entries(responseInit.headers)) {
res.setHeader(name, value);
}
}
res.statusCode = responseInit.status || 200;
// Using `.send` is a best practice for Express, but we also just use
// `.end` for compatibility with `connect`.
if (typeof res.send === 'function') {
res.send(graphqlResponse);
} else {
res.end(graphqlResponse);
}
},
(error: Error) => {
if (!isHttpQueryError(error)) {
return next(error);
}
if (error.headers) {
for (const [name, value] of Object.entries(error.headers)) {
res.setHeader(name, value);
}
}
res.statusCode = error.statusCode;
if (typeof res.send === 'function') {
// Using `.send` is a best practice for Express, but we also just use
// `.end` for compatibility with `connect`.
res.send(error.message);
} else {
res.end(error.message);
}
},
);
});
return router;
}
}
function prefersHtml(req: express.Request): boolean {
if (req.method !== 'GET') {
return false;
}
const accept = accepts(req);
const types = accept.types() as string[];
return (
types.find((x: string) => x === 'text/html' || x === 'application/json') ===
'text/html'
);
}