Package Exports
- graphql-http
- graphql-http/package.json
Readme
graphql-http
Simple, pluggable, zero-dependency, GraphQL over HTTP Protocol compliant server and client.
Need subscriptions? Try graphql-ws or graphql-sse instead!
Getting started
Install
yarn add graphql-http
Create a GraphQL schema
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';
/**
* Construct a GraphQL schema and define the necessary resolvers.
*
* type Query {
* hello: String
* }
*/
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'world',
},
},
}),
});
Start the server
With http
import http from 'http';
import { createHandler } from 'graphql-http';
import { schema } from './previous-step';
// Create the GraphQL over HTTP handler
const handler = createHandler({ schema });
// Create a HTTP server using the handler on `/graphql`
const server = http.createServer(async (req, res) => {
if (!req.url.startsWith('/graphql')) {
return res.writeHead(404).end();
}
try {
const [body, init] = await handler({
url: req.url,
method: req.method,
headers: req.headers,
body: await new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => resolve(body));
}),
raw: req,
});
res.writeHead(init.status, init.statusText, init.headers).end(body);
} catch (err) {
res.writeHead(500).end(err.message);
}
});
server.listen(4000);
console.log('Listening to port 4000');
With http2
Browsers might complain about self-signed SSL/TLS certificates. Help can be found on StackOverflow.
$ openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pem
import fs from 'fs';
import http2 from 'http2';
import { createHandler } from 'graphql-http';
import { schema } from './previous-step';
// Create the GraphQL over HTTP handler
const handler = createHandler({ schema });
// Create a HTTP/2 server using the handler on `/graphql`
const server = http2.createSecureServer(
{
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem'),
},
async (req, res) => {
if (!req.url.startsWith('/graphql')) {
return res.writeHead(404).end();
}
try {
const [body, init] = await handler({
url: req.url,
method: req.method,
headers: req.headers,
body: await new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => resolve(body));
}),
raw: req,
});
res.writeHead(init.status, init.statusText, init.headers).end(body);
} catch (err) {
res.writeHead(500).end(err.message);
}
},
);
server.listen(4000);
console.log('Listening to port 4000');
With express
import express from 'express'; // yarn add express
import { createHandler } from 'graphql-http';
import { schema } from './previous-step';
// Create the GraphQL over HTTP handler
const handler = createHandler({ schema });
// Create an express app serving all methods on `/graphql`
const app = express();
app.use('/graphql', async (req, res) => {
try {
const [body, init] = await handler({
url: req.url,
method: req.method,
headers: req.headers,
body: await new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => resolve(body));
}),
raw: req,
});
res.writeHead(init.status, init.statusText, init.headers).end(body);
} catch (err) {
res.writeHead(500).end(err.message);
}
});
app.listen(4000);
console.log('Listening to port 4000');
With fastify
import Fastify from 'fastify'; // yarn add fastify
import { createHandler } from 'graphql-http';
import { schema } from './previous-step';
// Create the GraphQL over HTTP handler
const handler = createHandler({ schema });
// Create a fastify instance serving all methods on `/graphql`
const fastify = Fastify();
fastify.all('/graphql', async (req, res) => {
try {
const [body, init] = await handler({
url: req.url,
method: req.method,
headers: req.headers,
body: await new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => resolve(body));
}),
raw: req,
});
res.writeHead(init.status, init.statusText, init.headers).end(body);
} catch (err) {
res.writeHead(500).end(err.message);
}
});
fastify.listen(4000);
console.log('Listening to port 4000');
With Deno
import { serve } from 'https://deno.land/std@0.151.0/http/server.ts';
import { createHandler } from 'https://esm.sh/graphql-http';
import { schema } from './previous-step';
// Create the GraphQL over HTTP handler
const handler = createHandler<Request>({ schema });
// Start serving on `/graphql` using the handler
await serve(
async (req: Request) => {
const [path, _search] = req.url.split('?');
if (!path.endsWith('/graphql')) {
return new Response(null, { status: 404, statusText: 'Not Found' });
}
const headers: Record<string, string> = {};
req.headers.forEach((value, key) => (headers[key] = value));
const [body, init] = await handler({
url: req.url,
method: req.method,
headers,
body: await req.text(),
raw: req,
});
return new Response(body, init);
},
{
port: 4000,
},
);
// Listening to port 4000
Use the client
import { createClient } from 'graphql-http';
const client = createClient({
url: 'http://localhost:4000/graphql',
});
(async () => {
let cancel = () => {
/* abort the request if it is in-flight */
};
const result = await new Promise((resolve, reject) => {
let result;
cancel = client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});
expect(result).toEqual({ hello: 'world' });
})();
Recipes
🔗 Client usage with Promise
import { ExecutionResult } from 'graphql';
import { createClient, RequestParams } from 'graphql-http';
import { getSession } from './my-auth';
const client = createClient({
url: 'http://hey.there:4000/graphql',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});
function execute<Data, Extensions>(
params: RequestParams,
): [request: Promise<ExecutionResult<Data, Extensions>>, cancel: () => void] {
let cancel!: () => void;
const request = new Promise<ExecutionResult<Data, Extensions>>(
(resolve, reject) => {
let result: ExecutionResult<Data, Extensions>;
cancel = client.subscribe<Data, Extensions>(params, {
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
});
},
);
return [request, cancel];
}
(async () => {
const [request, cancel] = execute({
query: '{ hello }',
});
// just an example, not a real function
onUserLeavePage(() => {
cancel();
});
const result = await request;
expect(result).toBe({ data: { hello: 'world' } });
})();
🔗 Client usage with Observable
import { Observable } from 'relay-runtime';
// or
import { Observable } from '@apollo/client/core';
// or
import { Observable } from 'rxjs';
// or
import Observable from 'zen-observable';
// or any other lib which implements Observables as per the ECMAScript proposal: https://github.com/tc39/proposal-observable
import { createClient } from 'graphql-http';
import { getSession } from './my-auth';
const client = createClient({
url: 'http://graphql.loves:4000/observables',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});
const observable = new Observable((observer) =>
client.subscribe({ query: '{ hello }' }, observer),
);
const subscription = observable.subscribe({
next: (result) => {
expect(result).toBe({ data: { hello: 'world' } });
},
});
// unsubscribe will cancel the request if it is pending
subscription.unsubscribe();
🔗 Client usage with Relay
import { GraphQLError } from 'graphql';
import {
Network,
Observable,
RequestParameters,
Variables,
} from 'relay-runtime';
import { createClient } from 'graphql-http';
import { getSession } from './my-auth';
const client = createClient({
url: 'http://i.love:4000/graphql',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});
function fetch(operation: RequestParameters, variables: Variables) {
return Observable.create((sink) => {
if (!operation.text) {
return sink.error(new Error('Operation text cannot be empty'));
}
return client.subscribe(
{
operationName: operation.name,
query: operation.text,
variables,
},
sink,
);
});
}
export const network = Network.create(fetch);
🔗 Client usage with Apollo
import {
ApolloLink,
Operation,
FetchResult,
Observable,
} from '@apollo/client/core';
import { print, GraphQLError } from 'graphql';
import { createClient, ClientOptions, Client } from 'graphql-http';
import { getSession } from './my-auth';
class HTTPLink extends ApolloLink {
private client: Client;
constructor(options: ClientOptions) {
super();
this.client = createClient(options);
}
public request(operation: Operation): Observable<FetchResult> {
return new Observable((sink) => {
return this.client.subscribe<FetchResult>(
{ ...operation, query: print(operation.query) },
{
next: sink.next.bind(sink),
complete: sink.complete.bind(sink),
error: sink.error.bind(sink),
},
);
});
}
}
const link = new HTTPLink({
url: 'http://where.is:4000/graphql',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});
🔗 Client usage with request retries
import { createClient, NetworkError } from 'graphql-http';
const client = createClient({
url: 'http://unstable.service:4000/graphql',
shouldRetry: async (err: NetworkError, retries: number) => {
if (retries > 3) {
// max 3 retries and then report service down
return false;
}
// try again when service unavailable, could be temporary
if (err.response?.status === 503) {
// wait one second (you can alternatively time the promise resolution to your preference)
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
}
// otherwise report error immediately
return false;
},
});
🔗 Client usage in browser
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>GraphQL over HTTP</title>
<script
type="text/javascript"
src="https://unpkg.com/graphql-http/umd/graphql-http.min.js"
></script>
</head>
<body>
<script type="text/javascript">
const client = graphqlHttp.createClient({
url: 'http://umdfor.the:4000/win/graphql',
});
// consider other recipes for usage inspiration
</script>
</body>
</html>
🔗 Client usage in Node
const fetch = require('node-fetch'); // yarn add node-fetch
const { AbortController } = require('node-abort-controller'); // (node < v15) yarn add node-abort-controller
const { createClient } = require('graphql-http');
const client = createClient({
url: 'http://no.browser:4000/graphql',
fetchFn: fetch,
abortControllerImpl: AbortController, // node < v15
});
// consider other recipes for usage inspiration
🔗 Client usage in Deno
import { createClient } from 'graphql-http';
const client = createClient({
url: 'http://deno.earth:4000/graphql',
});
// consider other recipes for usage inspiration
🔗 Server handler usage with authentication
Authenticate the user within graphql-http
during GraphQL execution context assembly. This is a approach is less safe compared to early authentication (see early authentication in Node) because some GraphQL preparations or operations are executed even if the user is not unauthorized.
import { createHandler } from 'graphql-http';
import {
schema,
getUserFromCookies,
getUserFromAuthorizationHeader,
} from './my-graphql';
const handler = createHandler({
schema,
context: async (req) => {
// process token, authenticate user and attach it to your graphql context
const userId = await getUserFromCookies(req.headers.cookie);
// or
const userId = await getUserFromAuthorizationHeader(
req.headers.authorization,
);
// respond with 401 if the user was not authenticated
if (!userId) {
return [null, { status: 401, statusText: 'Unauthorized' }];
}
// otherwise attach the user to the graphql context
return { userId };
},
});
🔗 Server handler usage with custom context value
import { createHandler } from 'graphql-http';
import { schema, getDynamicContext } from './my-graphql';
const handler = createHandler({
schema,
context: async (req, args) => {
return getDynamicContext(req, args);
},
// or static context by supplying the value direcly
});
🔗 Server handler usage with custom execution arguments
import { parse } from 'graphql';
import { createHandler } from 'graphql-http';
import { getSchemaForRequest, myValidationRules } from './my-graphql';
const handler = createHandler({
onSubscribe: async (req, params) => {
const schema = await getSchemaForRequest(req);
const args = {
schema,
operationName: params.operationName,
document: parse(params.query),
variableValues: params.variables,
};
return args;
},
});
🔗 Server handler usage in Node with early authentication (recommended)
Authenticate the user early, before reaching graphql-http
. This is the recommended approach because no GraphQL preparations or operations are executed if the user is not authorized.
import { createHandler } from 'graphql-http';
import {
schema,
getUserFromCookies,
getUserFromAuthorizationHeader,
} from './my-graphql';
const handler = createHandler({
schema,
context: async (req) => {
// user is authenticated early (see below), simply attach it to the graphql context
return { userId: req.raw.userId };
},
});
const server = http.createServer(async (req, res) => {
if (!req.url.startsWith('/graphql')) {
return res.writeHead(404).end();
}
try {
// process token, authenticate user and attach it to the request
req.userId = await getUserFromCookies(req.headers.cookie);
// or
req.userId = await getUserFromAuthorizationHeader(
req.headers.authorization,
);
// respond with 401 if the user was not authenticated
if (!req.userId) {
return res.writeHead(401, 'Unauthorized').end();
}
const [body, init] = await handler({
url: req.url,
method: req.method,
headers: req.headers,
body: await new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => resolve(body));
}),
raw: req,
});
res.writeHead(init.status, init.statusText, init.headers).end(body);
} catch (err) {
res.writeHead(500).end(err.message);
}
});
server.listen(4000);
console.log('Listening to port 4000');
Documentation
Check the docs folder out for TypeDoc generated documentation.
Want to help?
File a bug, contribute with code, or improve documentation? Read up on our guidelines for contributing and drive development with yarn test --watch
away!