Short version
- code Github
Long version
Highlights:
- starting with settin up QWIK and Apollo server
- we'll add a couple of routes
- one to expose the graphql as an endpoint
- one to consume it
- in the endpoint we'll use code inspired by the expressMiddlware for Apollo server 4 and update it so it works with the QWIK
RequestHandler
interface - on the consuming side
- fetch will need a slighly different URL when working on the server (SSR) vs the client so we'll use the
onRequest
androuteLoader
to provide the URL from QWIK down to thegqlCall
function
- fetch will need a slighly different URL when working on the server (SSR) vs the client so we'll use the
0. Setting up QWIK
1. Set up routes
- one route for the graphql endpoint
- so create a file
- one for a page that will consume that GQL
- branch in GitHub
2. Setting up Apollo
- Apollo getting started docs
npm install @apollo/server graphql
- Typescript and node types already installed and tsconfig configured by qwik so we can skip that
- create the
./graphql/index.ts
file - start with the schema
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 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]
}
`;
- add a resolver and the server
import { ApolloServer } from '@apollo/server';
// A schema is a collection of type definitions (hence "typeDefs")
// that together define the "shape" of queries that are executed against
// your data.
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]
}
`;
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,
},
};
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers,
});
export const serverStarted = server.start();
- on the last line we export the serverStarted which we'll use in the next step to expose the GraphQL API and UI
- branch in GitHub
3. Starting the server in an endpoint
- we have the endpoint in the
src/routes/graphql/index.ts
file - the following is inspired and using the code from expressMiddleware
@apollo/server/src/express4/index.ts
import { ContextFunction, BaseContext, HeaderMap, HTTPGraphQLRequest } from '@apollo/server';
import { RequestHandler } from '@builder.io/qwik-city';
import { serverStarted, server } from '~/graphql';
export const onRequest: RequestHandler = async ({
parseBody,
method,
next,
send,
request,
query,
getWritableStream,
}) => {
await serverStarted;
// from expressMiddleware node_modules/@apollo/server/src/express4/index.ts
server.assertStarted('QWIK Endpoint');
// This `any` is safe because the overload above shows that context can
// only be left out if you're using BaseContext as your context, and {} is a
// valid BaseContext.
const defaultContext: ContextFunction<[{}], BaseContext> = async () => ({});
return parseBody().then((body) => {
const headers = new HeaderMap(request.headers);
const httpGraphQLRequest: HTTPGraphQLRequest = {
method: method.toUpperCase(),
headers,
search: query.toString() ?? '',
body: body ?? {},
};
return server
.executeHTTPGraphQLRequest({
httpGraphQLRequest,
context: () => defaultContext({}),
})
.then(async (httpGraphQLResponse) => {
if (httpGraphQLResponse.body.kind === 'complete') {
const response = new Response(httpGraphQLResponse.body.string, {
status: 200,
headers: [...httpGraphQLResponse.headers.entries()],
});
send(response);
return;
}
const writableStream = getWritableStream();
const writer = writableStream.getWriter();
const encoder = new TextEncoder();
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
writer.write(encoder.encode(chunk));
}
writer.close();
})
.catch((e) => {
console.error(e);
next();
});
});
};
- this basically let's the Apollo server have the body and headers
-
and then relays the response back to the client
- in one go - if the
kind === complete
- otherwise - in a stream
- in one go - if the
branch in GitHub
4. Consume from a page
- in the
src/routes/page/index.tsx
we have the placeholder of a page - in the
src/routes/page/gql-call.ts
let's create a function to make a gql call
-
export function gqlCall<T>(
url: string,
body: string,
controller?: AbortController
): Promise<{ data?: T; errors?: unknown[] }> {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ query: body }),
signal: controller?.signal,
})
.then((r) => {
if (r.status === 200) {
const value = r.json();
return value;
}
return r.body
?.getReader()
.read()
.then((v) => {
throw new Error(`An error occurred: , /n ${r.statusText} /n${String.fromCodePoint(...(v.value ?? []))}`);
});
})
.catch((e) => console.error(e));
}
-
it adds the necessary minimum headers, reads the body, and has a minimal error handling
- now we can make a call in the page's component using the Qwik Resource primitive/component
import { Resource, component$, useResource$, useSignal, useTask$ } from '@builder.io/qwik'; import { gqlCall } from './gql-call'; const titlesQuery = `#gql query BookTitlesQuery { books { title } } ` export default component$(() => { const reload = useSignal(0) const res = useResource$(({track}) => { track(reload) return gqlCall<{ books: { title: string }[] }>(JSON.stringify({ query: titlesQuery })); }) return ( <> <button onClick$={() => reload.value +=1}>:reload:</button> <Resource value={res} onResolved={({ data }) => data != null && Array.isArray(data?.books) ? <>{data.books.map(b => <div> {b.title}</div>)}</> : <div>{JSON.stringify(data)}</div> } /> </> ); });
- branch in GitHub
5. Import the GraphQL schema
In a Real-worldยฎ scenario we would probably read that off of an environment variable:
const typeDefsFile = process.env.GRAPHQL_SCHEMA
To consume the GraphQL schema from a file we need a couple of changes:
- starting with the external file
src/graphql/schema.graphql
- for this demo we'll just copy over the book schema from
index.ts
- for this demo we'll just copy over the book schema from
- then in our
src/graphql/index.ts
we can
...
import typeDefs from './schema.graphql?raw';
....
- so at runtime
typeDefs
will hold the raw contents of the schema.graphql file allowing for the Apollo server to be built - branch in GitHub
Summary
Thus we have a single process that runs both the QWIK ssr and client code as well as the GraphQL server. We can use the endpoint /graphql by the app or externally.
Cheers.
๐งโ๐ Thanks for reading!
Give some ๐ or ๐ฆ if you liked this post.
These help other people find this post and encourage me to write more. Thanks!
Check out my projects:
- SCuri โ Unit test boilerplate automation (with Enterprise support option too)
- ngx-forms-typed โ Angular form, only strong typed!
- ngx-show-form-control โ Visualize/Edit any FormControl/Group
Top comments (0)