DEV Community

Georgi Parlakov
Georgi Parlakov

Posted on

How to Run Apollo GraphQL in a QWIK Endpoint

QWIK + Apollo server

Short version

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 and routeLoader to provide the URL from QWIK down to the gqlCall function

0. Setting up QWIK

  • start by setting up QWIK
  • npm create qwik@latest
    • we used the empty app template
  • branch in GitHub

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]
    }
    `;
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode
  • 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();
        });
    });
  };

Enter fullscreen mode Exit fullscreen mode
  • 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 GraphQL Sandbox - dev-only by default
  • 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));
    }

Enter fullscreen mode Exit fullscreen mode
  • 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>
                    }
                />
            </>
        );
    });
    
    

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
  • then in our src/graphql/index.ts we can
...
import typeDefs from './schema.graphql?raw';
....
Enter fullscreen mode Exit fullscreen mode
  • 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:

Buy me a coffee

Top comments (0)