Not to break out of the most common web-development cliché of using animals as a subject of a fun side-project, we will build a simple dog shelter website. For now, it will consist of three routes: the homepage, the general /dogs view and the details view of a singular dog. In this blog-post, the only action we will do API-wise is reading the data with a GraphQL query - no mutations or subscriptions (just yet).

The general goal is to present a neat foundation for an SSR React application that you can later use for some bigger projects. We will use Next.js + TypeScript as the UI framework and our data will be stored in a service called Hasura, from which we will fetch it through automatically generated GraphQL API.

I will not spend too much time on doing anything flashy, at least not in the beginning of this series, so if you are here to feast your eyes I guess the only thing I can recommend you to do is scrolling to the bottom of this page and meeting with a handsome fellow there. The direction of this initiative however is completely up to you - if you feel like you want me to go some place with it, just let me know on Twitter: @pilarenko.

You can expect me not to leave you out in the cold, skipping multiple steps in the process and assuming you know everything, but I must warn you the elementary React, TypeScript and GraphQL knowledge is advised. I will do my best to follow the best practices in my usage of these tools but feel free to create a Pull Request if anything needs some extra work. The repository is available here.

Agenda

  1. Set up Next.js + TypeScript application
    1. Basic setup (can be skipped with npx create-next-app)
    2. Adding TypeScript
    3. Adding routes
  2. Set up Hasura backend
  3. Next.js + Hasura
    1. Environment variables
    2. From server to client
    3. Apollo configuration
    4. useFetch
    5. Fetch data on the server-side
  4. Deploy

Useful links


1. Set up Next.js + TypeScript application

1.1 Basic setup

I decided to boot up the entire project from scratch because 1) I think it may be educational 2) the app is so simple there is no need to grab a template from the official Next.js repository, only to plow half of the boilerplate in the first step. So let's roll with the good old:

npm init

You can "enter" your way through it, as the questions asked by the CLI are not super relevant for our project. We end up with a package.json that desperately needs its first dependencies. Let's bring them:

npm install next react react-dom

Let's also create a .gitignore file and add our chunky boi node_modules there.

touch .gitignore
# .gitignore

/node_modules

Then, we need to edit package.json to add Next scripts needed to run the project or build it.

// package.json

{
	...
	"scripts": {
	    "dev": "next dev",
	    "build": "next build",
	    "start": "next start"
	  },
	...
}

The structure of Next.js application revolves around the usage of pages directory for defining the routes. This means each .jsx/.tsx file, no matter how deeply nested inside their own subfolders, equals a subroute. For now, let's create the homepage ("/"), which Next.js expects it to be pages/index.jsx.

mkdir pages
touch index.jsx

And let's fill it up with some basic React:

// pages/index.jsx

import React from 'react'

const Index = () => {
  return (
    <div>
      <h1>Homepage</h1>
    </div>
  )
}

export default Index

Didn't I tell you there was going to be TypeScript? Don't worry, I remember. Let's just quickly check if everything works as expected and we will convert it to .tsx. Run:

npm run dev

and hopefully witness this beauty:

But that's not the only thing you will witness - your eye will probably catch the appearance of .next folder.

What lays inside is all of the dev build files that Next.js generates for us to conveniently work with the application locally. Don't look into it, it's written in elvish. Best you can do is add .next to .gitignore.

1.2 Adding TypeScript

Now, let's finally get to TypeScript and make use of that sweet Next.js magic. Let's type:

touch tsconfig.json

to create a file that will be used to specify compiler options for TypeScript. And from this moment, Next.js will guide you through the rest. First, you don't even have to worry about filling the config, because as soon as you fire npm run dev again, it will automatically generate it. Next time you look at it, you will see something similar to this:

// tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

Then, check the console for the list of packages required to finish the setup. Most likely, it will be the following:

npm install --save-dev typescript @types/react @types/node

The first one is the star itself, followed by type definitions for React and Node that we can leverage in our development environment.

Pretty nifty, right? After we have tsconfig covered, it's finally time to pull the trigger on that extension change, so let's turn index.jsx to index.tsx.

Changing the extension doesn't result in anything by itself, but I suggest we start our beautiful love-story between React and TypeScript by adding the type of React Functional Component (React: FC) to our Index function. It doesn't do anything just yet but that's usually the story with TypeScript - it's useless until it is not.

// pages/index.tsx

import React from 'react';

const Index: React.FC = () => {
  return (
    <div>
      <h1>Homepage</h1>
    </div>
  );
};

export default Index;

1.3 Adding routes

Now that we have our index page, let's add the remaining subroutes, starting with /dogs.

// pages/dogs.tsx

import React from 'react';

const Dogs: React.FC = () => {
  return (
    <div>
      <h1>D'ya like dagz?</h1>
    </div>
  );
};

export default Dogs;

The way I envision the application to work is to redirect the user to dog-specific details page after they click on the dog's thumbnail photo in the /dogs page. For that, we need the said dog details page that will be recognized by the dog's id. We expect the URL to look something like: www.dog-shelter.com/dog/f1297fb9-1097-45be-a0a3-6ca63f5a8ed9, where after the "/dog/" part we have the uuid, which is dynamic - it is different for every dog. We can translate that to Next.js routes through the bracket syntax, where we will keep the dynamic parameter in a [bracket]. So, the path for our singular dog component should look like this:

// pages/dog/[id].tsx

Temporarily, all we will do is read the "dog's" id from the URL with the help of useRouter hook and display it inside the component. It can look like this:

// pages/dog/[id].tsx

import React from 'react';
import { useRouter } from 'next/router';

const Dog: React.FC = () => {
  const router = useRouter();
  const { id } = router.query;

  return (
    <div>
      <h2>{`Dog: ${id}`}</h2>
    </div>
  );
};

export default Dog;

So our current pages folder should follow this structure:

Now, we have a basic Next.js application with three routes and a non-existing UI. Yaay. Time to make it a bit more exciting.

2. Hasura

Nothing translates the spirit of JamStack better than Hasura. I recommend fastening your seatbelt, as thanks to that service, we are about to create a full backend with GraphQL API in a matter of minutes.

Hasura does wonders with exposing to you as little as it is needed to maintain a database, API and even some back-end logic, although it certainly keeps some doors open if you are interested in snooping around. We will keep it basic for now, so let's just get down to the point.

Please visit https://cloud.hasura.io/ and log in with the service of your choice, then go for "Try a free database with Heroku". Heroku may be a bit forgotten in the JamStack universe, but it definitely does the trick here. You need to create an account (if you don't already have it) though.

Right in the end of the process, click "Create Project" and you should at last land in Hasura dashboard.

After the service was successfully initialized, proceed and go straight to the "Data" page, where we can finally get our hands dirty. Click on "Add Table" where we will be able to add some columns to our table and therefore, thanks to Hasura and GraphQL magic, to our API. This may feel a bit overwhelming if you have never worked with a database before, but don't worry, Hasura will make it as easy as it is possible.

Let's start with giving our table a name (in my case: "dogs") and adding a bunch of fields. Notice that Hasura UI offers some smart autocompletion based on what kind of database field type you chose (f.e. now() for the Timestamp).

I suggest to make use of their "Frequently used columns" section and grab id with the UUID preset, created_at and updated_at, although feel free to play around with it as you like. The rest of the fields are up to you, but I will stick to my "dogs" theme and add name, age, bio and image. Don't sweat it too much, because you will be able to modify it later on.

Just make sure you pick "id" as the primary key of your table.

Then go to our newly created table and click on "Insert Row" tab to fill it with a first record. If you have fields with specified default values, they will be used from the start, so in my case I have to worry only about "name", "age", "image" and "bio".

Done! And I really mean done - the only thing you can do right now on the Hasura's side is to play around with the GraphiQL interface to query your API.

As your experience with GraphQL APIs will grow, you will get more and more acquainted with this view: meet GraphiQL. This is your interactive GraphQL playground, from which you can build a GraphQL query even without an extensive knowledge of the language. You should be able to access your table from the "Explorer" page, right where I have my "dogs". By clicking on the checkboxes, you can automatically add a field to the query, in the vein of the GraphQL credo: you fetch only what you need.

3. Next.js + Hasura

3.1 Environment variables

Now, let's head back to our Next.js application.

To not to be bothered with it in the future, we will start by adding the Hasura endpoint URL to our project as an environment variable. Luckily, Next.js made massive improvements in that regard in one of the latest releases, so it's as easy as:

  1. creating .env file
  2. adding it to .gitignore
  3. storing our URL there remembering that 1) it has to follow the naming convention "NEXT_PUBLIC_XYZ", so that it's accessible on both the client and the server 2) don't copy the "https://" protocol, because we want to be flexible with the URL.

Therefore, our .env should look like this:

// .env

NEXT_PUBLIC_API_HOST=abc.hasura.app/v1/graphql

3.2 From server to client

Next.js shines the most, when you properly leverage its capabilities on the data-fetching side. The framework offers you a bunch of methods that you can run on pages/ route components that define in what way this piece of application will be built and served. For example, if you decide to use getServerSideProps - a function that works like a "window" to the server-side realm of Next.js - this entire page will be automatically server-side rendered.

The perfect scenario for such page would be when, during the build, we are not aware of the number and the content of all the URLs under this subroute, as they can be constantly added. Think of a large e-commerce website, where new products and offers can be added/removed each minute.

Our use-case is more straight-forward - the number of dogs in the shelter will not change too much, so we will be able to render it statically. This means, in the future we will rather work with getInitialProps or getStaticProps method, but to showcase they do not differ too much in the implementation, let's add getServerSideProps here for a second.

// pages/dogs/[id].tsx

import React from 'react';
import { useRouter } from 'next/router';

const Dog: React.FC = () => {
  const router = useRouter();
  const { id } = router.query;

  return (
    <div>
      <h2>{`Dog: ${id}`}</h2>
    </div>
  );
};

export async function getServerSideProps() {
  const dogName = 'Scooby-Doo';
  console.log(dogName);

  return {
    props: {
      foo: 'bar',
    },
  };
}

export default Dog;

Notice: console.log doesn't show in the browser, but...

...in the terminal, on the server-side, it does.

So let's pass a hard-coded "Scooby-Doo" string to our UI.

// pages/dogs/[id].tsx

...

export async function getServerSideProps() {
  const dogName = 'Scooby-Doo';

  return {
    props: {
      name: dogName,
    },
  };
}
// pages/dogs/[id].tsx

...

const Dog = ({ name }) => {
  const router = useRouter();
  const { id } = router.query;

  return (
    <div>
      <h2>{`Dog: ${id}, name: ${name}`}</h2>
    </div>
  );
};

...

3.3 Apollo configuration

While this pattern is completely useless for real-world application, the idea of passing the data from the server-side to the client will be used once we start fetching it from the API. And for that, we need Apollo:

npm install --save @apollo/client graphql

Apollo can be best described as a bridge between the UI framework (this role is played by React/Next.js in our stack) and GraphQL. This colossus of a library offers a variety of features for both the creation of GraphQL server, as well as fetching the data from an existing GraphQL API.

There is no golden standard for data-fetching (server-side vs. client-side), as in practice it usually ends up being the combination of both. This is why I want to show you first how can we do it in the browser with the usage of useQuery hook. To make it possible, we first have to initialize our ApolloClient and spread it across the entire application. Thankfully, Next.js has a component just for that, called pages/_app.tsx . You can think of it as a wrapper that wraps every page.

Let's take care of our ApolloClient first:

// pages/_app.tsx

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: `https://${process.env.NEXT_PUBLIC_API_HOST}`,
  cache: new InMemoryCache(),
});

As we can see, it doesn't need that much to work: only the URL of our Hasura Cloud GraphQL API (stored in an env var) and a new instance of InMemoryCache(), which takes care of caching the requests for us. Then, we can provide it to our routes like this:

// pages/_app.tsx

import React from 'react';
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { AppProps } from 'next/app';

const client = new ApolloClient({
  uri: `https://${process.env.NEXT_PUBLIC_API_HOST}`,
  cache: new InMemoryCache(),
});

const App = ({ Component, pageProps }: AppProps) => (
  <ApolloProvider client={client}>
    <Component {...pageProps} />
  </ApolloProvider>
);

export default App;

3.4 useFetch

And then onto our pages/dogs, where as the original plan assumed, we want to fetch all of the dogs and display some basic information about them. This is where the GraphiQL playground will come in handy - we can specify our query there and just copy it to our editor once it's complete. In order to make it readable for Apollo, we have to wrap it with a gql function though:

// pages/dogs.tsx

import { gql } from '@apollo/client';

const GET_DOGS = gql`
  query GetDogs {
    dogs {
      id
      name
      age
      bio
    }
  }
`;

Then, let's bring in previously announced hook called useQuery. Supplied with the GraphQL server configuration, thanks to the work we did in the _app.tsx, it is now able to operate without much more parameters. The only thing we have to pass to it is our query, in my case called GET_DOGS, and the response type (DogsResponseData). This hook returns three things that shouldn't raise an eyebrow of anyone who's ever queried an API: loading, error and data.

// pages/dogs.tsx

...

type DogsResponseData = {
  dogs: Dog[];
};

const Dogs = () => {
  const { loading, error, data } = useQuery<DogsResponseData>(GET_DOGS);

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

	...
}

The first one is a boolean - if it is true, we should display some sort of a loading component. The second one is an instance of ApolloError - if it is not nullish, it means something went wrong and the user should be served with an appropriate feedback. If both are false it means we can finally unpack the data and display it in the UI:

// pages/dogs.tsx

import { gql, useQuery } from '@apollo/client';
import React from 'react';

const GET_DOGS = gql`
  query GetDogs {
    dogs {
      id
      name
      age
      bio
    }
  }
`;

type DogsResponseData = {
  dogs: Dog[];
};

const Dogs = () => {
  const { loading, error, data } = useQuery<DogsResponseData>(GET_DOGS);

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  return (
    <div>
      <h1>Dogs:</h1>
      {data.dogs.map((dog) => (
        <ul key={`dog-${index}`}>
          <li>{`name: ${dog.name}`}</li>
          <li>{`age: ${dog.age}`}</li>
          <li>{`bio: ${dog.bio}`}</li>
        </ul>
      ))}
    </div>
  );
};

export default Dogs;

After those changes, the entire component looks like this:

import { gql, useQuery } from '@apollo/client';
import React from 'react';

type Dog = {
  id: string;
  name: string;
  age: string;
  bio: string;
};

const GET_DOGS = gql`
  query GetDogs {
    dogs {
      id
      name
      age
      bio
    }
  }
`;

type DogsResponseData = {
  dogs: Dog[];
};

const Dogs = () => {
  const { loading, error, data } = useQuery<DogsResponseData>(GET_DOGS);

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  return (
    <div>
      <h1>Dogs:</h1>
      {data.dogs.map((dog, index) => (
        <ul key={`dog-${index}`}>
          <li>{`name: ${dog.name}`}</li>
          <li>{`age: ${dog.age}`}</li>
          <li>{`bio: ${dog.bio}`}</li>
        </ul>
      ))}
    </div>
  );
};

export default Dogs;

In my opinion it already looks tidy enough, but if we want to go full Marie Kondo, we can also create separate files for our queries and types and throw our query GET_DOGS and type Dog there.

3.5 Fetch data on the server-side

Here comes the last bit of the implementation we will do in this blog-post: querying the API on the server-side. For that, we will need to get some bigger guns: apolloClient.js and withApollo.jsx wrapper. To be fair, I didn't come up with them on my own - they are suggested in the official Hasura & Apollo & Next.js implementation documentation right here.

Unfortunately, the documentation assumes we will want to have an Authentication (auth0) in our application as well, which is not needed for the moment, so I decided to translate it to TypeScript, clean it up a bit and leave only what is essential for our simple demo.

Notice that we use our API URL environment variable (in my case it's NEXT_PUBLIC_API_HOST) here:

// lib/apolloClient.ts

import fetch from 'isomorphic-unfetch';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { NormalizedCacheObject } from '@apollo/client';

const createHttpLink = (): HttpLink => {
  const httpLink = new HttpLink({
    uri: `https://${process.env.NEXT_PUBLIC_API_HOST}`,
    credentials: 'include',
    fetch,
  });
  return httpLink;
};

const createWSLink = (): WebSocketLink => {
  return new WebSocketLink(
    new SubscriptionClient(`wss://${process.env.NEXT_PUBLIC_API_HOST}`, {
      lazy: true,
      reconnect: true,
    }),
  );
};

export default function createApolloClient(initialState: NormalizedCacheObject) {
  const ssrMode = typeof window === 'undefined';
  let link;
  if (ssrMode) {
    link = createHttpLink();
  } else {
    link = createWSLink();
  }
  return new ApolloClient({
    ssrMode,
    link,
    cache: new InMemoryCache().restore(initialState),
  });
}

TypeScript version of the withApollo wrapper was also nowhere to be found in the official documentation, but fortunately I was able to find it in a very helpful comment from samuelcastro on one of Next.js GitHub issues.

// lib/withApollo.tsx

import App, { AppContext } from 'next/app';
import Head from 'next/head';
import { ApolloProvider } from '@apollo/react-hooks';
import { ApolloClient } from 'apollo-client';
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
import { NextPageContext, NextPage } from 'next';
import createApolloClient from './apolloClient';

interface NextPageContextWithApollo extends NextPageContext {
  apolloClient: ApolloClient<NormalizedCacheObject> | null;
  apolloState: NormalizedCacheObject;
  ctx: NextPageContextApp;
}

type NextPageContextApp = NextPageContextWithApollo & AppContext;

// On the client, we store the Apollo Client in the following variable.
// This prevents the client from reinitializing between page transitions.
let globalApolloClient: ApolloClient<NormalizedCacheObject> | null = null;

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {NormalizedCacheObject} initialState
 * @param  {NextPageContext} ctx
 */
const initApolloClient = (initialState: NormalizedCacheObject) => {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === 'undefined') {
    return createApolloClient(initialState);
  }

  // Reuse client on the client-side
  if (!globalApolloClient) {
    globalApolloClient = createApolloClient(initialState);
  }

  return globalApolloClient;
};

/**
 * Installs the Apollo Client on NextPageContext
 * or NextAppContext. Useful if you want to use apolloClient
 * inside getStaticProps, getStaticPaths or getServerSideProps
 * @param {NextPageContext | NextAppContext} ctx
 */
export const initOnContext = (ctx: NextPageContextApp): NextPageContextApp => {
  const inAppContext = Boolean(ctx.ctx);

  // We consider installing `withApollo({ ssr: true })` on global App level
  // as antipattern since it disables project wide Automatic Static Optimization.
  if (process.env.NODE_ENV === 'development') {
    if (inAppContext) {
      console.warn(
        'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' +
          'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n',
      );
    }
  }

  // Initialize ApolloClient if not already done
  // TODO: Add proper types here:
  // https://github.com/zeit/next.js/issues/9542
  const apolloClient = ctx.apolloClient || initApolloClient(ctx.apolloState || {});

  // We send the Apollo Client as a prop to the component to avoid calling initApollo() twice in the server.
  // Otherwise, the component would have to call initApollo() again but this
  // time without the context. Once that happens, the following code will make sure we send
  // the prop as `null` to the browser.
  // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
  // @ts-ignore
  apolloClient.toJSON = () => null;

  // Add apolloClient to NextPageContext & NextAppContext.
  // This allows us to consume the apolloClient inside our
  // custom `getInitialProps({ apolloClient })`.
  ctx.apolloClient = apolloClient;
  if (inAppContext) {
    ctx.ctx.apolloClient = apolloClient;
  }

  return ctx;
};

/**
 * Creates a withApollo HOC
 * that provides the apolloContext
 * to a next.js Page or AppTree.
 * @param  {Object} withApolloOptions
 * @param  {Boolean} [withApolloOptions.ssr=false]
 * @returns {(PageComponent: ReactNode) => ReactNode}
 */
const withApollo = ({ ssr = false } = {}) => (PageComponent: NextPage): ReactNode => {
  const WithApollo = ({
    apolloClient,
    apolloState,
    ...pageProps
  }: {
    apolloClient: ApolloClient<NormalizedCacheObject>;
    apolloState: NormalizedCacheObject;
  }): ReactNode => {
    let client;
    if (apolloClient) {
      // Happens on: getDataFromTree & next.js ssr
      client = apolloClient;
    } else {
      // Happens on: next.js csr
      client = initApolloClient(apolloState);
    }

    return (
      <ApolloProvider client={client}>
        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
        <PageComponent {...pageProps} />
      </ApolloProvider>
    );
  };

  // Set the correct displayName in development
  if (process.env.NODE_ENV !== 'production') {
    const displayName = PageComponent.displayName || PageComponent.name || 'Component';
    WithApollo.displayName = `withApollo(${displayName})`;
  }

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async (ctx: NextPageContextApp): Promise<object> => {
      const inAppContext = Boolean(ctx.ctx);
      const { apolloClient } = initOnContext(ctx);

      // Run wrapped getInitialProps methods
      let pageProps = {};
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx);
      } else if (inAppContext) {
        pageProps = await App.getInitialProps(ctx);
      }

      // Only on the server:
      if (typeof window === 'undefined') {
        const { AppTree } = ctx;
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps;
        }

        // Only if dataFromTree is enabled
        if (ssr && AppTree) {
          try {
            // Import `@apollo/react-ssr` dynamically.
            // We don't want to have this in our client bundle.
            const { getDataFromTree } = await import('@apollo/react-ssr');

            // Since AppComponents and PageComponents have different context types
            // we need to modify their props a little.
            let props: any;
            if (inAppContext) {
              props = { ...pageProps, apolloClient };
            } else {
              props = { pageProps: { ...pageProps, apolloClient } };
            }

            // Take the Next.js AppTree, determine which queries are needed to render,
            // and fetch them. This method can be pretty slow since it renders
            // your entire AppTree once for every query. Check out apollo fragments
            // if you want to reduce the number of rerenders.
            // https://www.apollographql.com/docs/react/data/fragments/
            // eslint-disable-next-line react/jsx-props-no-spreading
            await getDataFromTree(<AppTree {...props} />);
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error('Error while running `getDataFromTree`', error);
          }

          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind();
        }
      }

      return {
        ...pageProps,
        // Extract query data from the Apollo store
        apolloState: apolloClient?.cache.extract(),
        // Provide the client for ssr. As soon as this payload
        apolloClient: ctx.apolloClient,
      };
    };
  }

  return WithApollo;
};

export default withApollo;

Once we have that covered, we can go to our _app.tsx and get rid of the initialization of the ApolloClient there, as we will do it on the page level from now on. We don't have to delete the whole file because it is likely we will need it at some point. After the cleanup, it should look similar to this:

// pages/_app.tsx

import React from 'react';
import { AppProps } from 'next/app';

const App = ({ Component, pageProps }: AppProps) => <Component {...pageProps} />;

export default App;

Now, the next step is replacing our client fetching logic with its server-side equivalent. We can keep the Dog type and the query intact, but we definitely need to import our withApollo with ssr setting set to true and wrap the component with it.

// pages/dogs.tsx

import withApollo from '../lib/withApollo';

...

export default withApollo({ ssr: true })(Dogs);

Okay, now it will get a bit more serious, but don't worry, we will break it down:

// pages/dogs.tsx

import { ApolloClient, gql, NormalizedCacheObject } from '@apollo/client';
import { NextPage, NextPageContext } from 'next';
import React from 'react';
import withApollo from '../lib/withApollo';

...

type ServerSideProps = NextPageContext & { apolloClient: ApolloClient<NormalizedCacheObject> };

type DogsResponse = {
  dogs: Dog[];
};

Dogs.getInitialProps = async ({ apolloClient }: ServerSideProps) => {
  const response = await apolloClient.query<DogsResponse>({
    query: GET_DOGS,
  });

  return {
    dogs: response.data.dogs,
  };
};

export default withApollo({ ssr: true })(Dogs);

First, let's have a look on the getInitialProps function that we access from our Dogs component. Why can we even do that if we wrote that component ourselves and have no recollection of such a function? The explanation is simple: it's Next.js and its magic tricks. Like I mentioned earlier, our routes in the pages/ folder have data-fetching superpowers and this is one of them.

To properly use it, we have to make TypeScript happy with changing the type of our Dogs component from:

const Dogs: React.FC = () => ...

to

const Dogs: NextPage = () => ...

which makes sense, because getInitialProps is only accessible from the Next.js page, not from a regular React functional component. TypeScript however doesn't know that it is looking at a Next.js page, even though it is in the pages/ directory, so we have to tell it explicitly.

Back to the function itself: it's just a regular async function, executed on the server side, that returns values we will be able to access on the client's side. It parameters consist of Next.js context object, called NextPageContext that we extend with our ApolloClient. Thus the definition for the ServerSideProps:

type ServerSideProps = NextPageContext & { apolloClient: ApolloClient<NormalizedCacheObject> };

Why do I know that ApolloClient has this very specific type? I acquired it the most straight-forward way possible - I copied it from the type definition of a return value of a createApolloClient function we have in our lib/apolloClient.js, even though the file is in plain .js. Bless you TypeScript, bless you VSCode.

Then, making the actual request is quite simple, we just have to remember about passing the response type to ApolloClient query method:

// pages/dogs.tsx

...
type DogsResponse = {
  dogs: Dog[];
};

Dogs.getInitialProps = async ({ apolloClient }: ServerSideProps) => {
	const response = await apolloClient.query<DogsResponse>({
    query: GET_DOGS,
  });

  return {
    dogs: response.data.dogs,
  };
...

The only difference is we can actually await the result and not bother with loading. Once it arrived, we dig the actual dogs out of it and return to our Dogs component, telling it that we will pass an array of Dog typed objects beforehand:

// pages/dogs.tsx

...

type Props = { dogs: Dog[] };

const Dogs: NextPage<Props> = ({ dogs }) => ...

In all of its glory, pages/dogs.tsx with data-fetching on the server-side looks like this:

// pages/dogs.tsx

import { ApolloClient, gql, NormalizedCacheObject } from '@apollo/client';
import { NextPage, NextPageContext } from 'next';
import React from 'react';
import withApollo from '../lib/withApollo';

type Dog = {
  id: string;
  name: string;
  age: string;
  bio: string;
};

const GET_DOGS = gql`
  query GetDogs {
    dogs {
      id
      name
      age
      bio
    }
  }
`;

type Props = { dogs: Dog[] };

const Dogs: NextPage<Props> = ({ dogs }) => {
  return (
    <div>
      <h1>Dogs:</h1>
      {dogs.map((dog, index) => (
        <ul key={`dog-${index}`}>
          <li>{`name: ${dog.name}`}</li>
          <li>{`age: ${dog.age}`}</li>
          <li>{`bio: ${dog.bio}`}</li>
        </ul>
      ))}
    </div>
  );
};

type ServerSideProps = NextPageContext & { apolloClient: ApolloClient<NormalizedCacheObject> };

type DogsResponse = {
  dogs: Dog[];
};

Dogs.getInitialProps = async ({ apolloClient }: ServerSideProps) => {
	const response = await apolloClient.query<DogsResponse>({
    query: GET_DOGS,
  });

  return {
    dogs: response.data.dogs,
  };
};

export default withApollo({ ssr: true })(Dogs);

And that's it! The result is basically the same, the only difference is we don't see the loading for a split second every time we access the page. The data is graciously fetched on the server-side and rendered in the component, as if it was always there.

5. Deploy

Don't be shy! This state-of-the-art application definitely deserves to be bragged about, so let's brag. For deployment, we will grab one last tool that will make this process extremely pleasant: Vercel. Let's install its CLI globally on our machine:

npm i -g vercel

and run this tiny command in the project directory to deploy it in the cloud:

vercel

I will not deprive you from the joy of interacting with Vercel, so no spoilers for how smooth this thing works. After you successfully set everything up, you will have to add one last thing to your production environment: your environment variables. It's as easy as going to the projects "Settings" and then to the "Environment Variables" tab and just pasting it there.

Boom! That's really it. You just created a full stack-JamStack application with routes, TypeScript, CI/CD, GraphQL API and SSR, you madman!

But the show most definitely must go on - in the next blog-posts, we will add some more CRUD operations to our dog shelter, spread our wings with GraphQL & Hasura, tackle more complex data-fetching scenarios and who knows, maybe even build a simple UI. Thanks for stopping by and see you in the next one!