Create a Fullstack Blog App with Next.js, Prisma 2 and Docker- Part II Configure the Frontend

Learn how to build a fullstack blog application. In Part II we hook up our frontend to our backend and add a beautiful UI with Ant Design.

In this post we are picking up right where we left off in Part I. If you haven't read the first post, you should do that first. As a reminder, we have already built our boilerplate frontend and backend web servers, and dockerized them. In this post we will configure our Frontend server to hook it up with our backend server using Apollo and we will add Ant Design for a nice site layout.

Where's the code?

We have the final code posted over on Github.

#Table of Contents:

Part I- Set up the Repo and Configure the Backend

  1. Create the base repo

  2. Create the Next.js frontend

  3. Create the boilerplate Prisma 2 backend

  4. Dockerize our web app

    a. Create a docker-compose file

    b. Add Dockerfiles for each container

  5. Configure the backend

    a. Switch database from SQLite to MySQL

    b. Remove unused backend boilerplate code

    c. Update backend queries in Prisma Nexus

  6. Verify our Docker-Compose cluster works

Part II- Configure the Frontend (this post)

  1. Add GraphQL fragments, queries and resolvers
  2. Add GraphQL-Codegen for generating React Components
  3. Add Apollo and create HOC for Next.js
  4. Add React-Apollo to project root
  5. Install Antd for a beautiful site layout

Part III- Build the UI

  1. Create the Main Layout
  2. Create a Users Layout Component
  3. Create a Signup User Component
  4. Create a Feed Layout Component
  5. Create a New Draft Component
  6. Create a Publish Draft Button Component
  7. Create a Delete Post Button Component

Just like before- be sure to check out the end for videos that walk through this whole process.

Part II- Configure the Frontend

1. Add our GraphQL Fragments, Queries and Resolvers

The goal of the first half of this post is to use code generation to create the most difficult React components for us. In order to do that, we need to first specify all the queries and mutations that our frontend will be using. This will include information about which input parameters are needed and which fields we wish to get back from our requests. We will create all of these using graphql files.

Add fragments

First, in order to encourage code reusibility, let's create fragments for our Post and User objects:

/frontend/graphql/fragments/post.gql

fragment PostFragment on Post {
  id
  published
  title
  content
  published
}

/frontend/graphql/fragments/user.gql

fragment UserFragment on User {
  id
  name
  email
}

Add queries

We can use these fragments in our queries and mutations. Let's start by creating our queries first:

/frontend/graphql/queries/feed.gql

#import from '../fragments/post.gql'

query feedQuery($published: Boolean!) {
  feed(published: $published) {
    ...PostFragment
  }
}

/frontend/graphql/queries/post.gql

#import from '../fragments/post.gql'

query postQuery($id: ID!) {
  post(where: { id: $id }) {
    ...PostFragment
  }
}

/frontend/graphql/queries/users.gql

#import from '../fragments/user.gql'

query usersQuery {
  users {
    ...UserFragment
  }
}

Add mutations

Now let's create our mutations:

/frontend/graphql/mutations/createDraft.gql

#import from '../fragments/post.gql'

mutation createDraftMutation(
  $title: String!
  $content: String!
  $authorEmail: String!
) {
  createDraft(title: $title, content: $content, authorEmail: $authorEmail) {
    ...PostFragment
  }
}

/frontend/graphql/mutations/deleteOnePost.gql

#import from '../fragments/post.gql'

mutation deleteOnePost($id: ID!) {
  deleteOnePost(where: { id: $id }) {
    ...PostFragment
  }
}

/frontend/graphql/mutations/publish.gql

#import from '../fragments/post.gql'

mutation publishMutation($id: ID!) {
  publish(id: $id) {
    ...PostFragment
  }
}

/frontend/graphql/mutations/signupUser.gql

#import from '../fragments/user.gql'

mutation signupUserMutation($name: String!, $email: String!) {
  signupUser(data: { name: $name, email: $email }) {
    ...UserFragment
  }
}

2. Add Graphql-Codegen to the frontend

Graphql-Codegen will take in our graphQL queries, mutations, and fragments and query against our backend server to create a generated file that contains React Components for all our possible Apollo operations that we could do with our backend server.

First install the codegen tools:

npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo graphql

Next, we have to add the codegen file which contains configuration for how the code generation should behave:

frontend/codegen.yml

overwrite: true
schema: 'http://backend:4000/'
documents: graphql/**/*.gql
generates:
  generated/apollo-components.tsx:
    config:
      withHOC: false
      withComponent: true
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'

Finally, add a npm script to the package.json file in our root:

/package.json

"generate": "docker exec -it frontend npm run generate",

and this npm script to your frontend/package.json:

"generate": "gql-gen --config codegen.yml"

Now run npm run generate from the root project. We can see that calling the generate script from the root will execute an npm run script call within our frontend container which will call the gql-gen tool.

Since we created a volume between our frontend app folder and the frontend folder in our computer, any files that are generated in the docker container will make their way to the host. It is for this reason that you should see that there is now a new file frontend/generated/apollo-components.tsx that has all the typescript types, graphql documents, and react components. It is almost 300 lines of code so it is so nice that we don't have go create this. Be sure to run the generate again if you ever add new files to the graphql folder on the frontend so that this file will regenerate for you.

3. Install Apollo and create HOC for Next.js

Within the frontend directory install the following libraries:

npm install --save apollo-boost isomorphic-unfetch react-apollo

Create the frontend/utils/init-apollo.js and frontend/utils/with-apollo-client.js files.

frontend/utils/init-apollo.js

import { ApolloClient, InMemoryCache, HttpLink } from 'apollo-boost'
import fetch from 'isomorphic-unfetch'

let apolloClient = null

function create(initialState) {
  // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
  const isBrowser = typeof window !== 'undefined'
  return new ApolloClient({
    connectToDevTools: isBrowser,
    ssrMode: !isBrowser, // Disables forceFetch on the server (so queries are only run once)
    link: new HttpLink({
      uri: isBrowser ? 'http://localhost:4000' : 'http://backend:4000', // Server URL (must be absolute)
      credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
      // Use fetch() polyfill on the server
      fetch: !isBrowser && fetch,
    }),
    cache: new InMemoryCache().restore(initialState || {}),
  })
}

export default function initApollo(initialState) {
  // 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 create(initialState)
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = create(initialState)
  }

  return apolloClient
}

frontend/utils/with-apollo-client.js

import React from 'react'
import initApollo from './init-apollo'
import Head from 'next/head'
import { getDataFromTree } from 'react-apollo'

export default App => {
  return class Apollo extends React.Component {
    static displayName = 'withApollo(App)'
    static async getInitialProps(ctx) {
      const { Component, router } = ctx

      let appProps = {}
      if (App.getInitialProps) {
        appProps = await App.getInitialProps(ctx)
      }

      // Run all GraphQL queries in the component tree
      // and extract the resulting data
      const apollo = initApollo()
      if (typeof window === 'undefined') {
        try {
          // Run all GraphQL queries
          await getDataFromTree(
            <App
              {...appProps}
              Component={Component}
              router={router}
              apolloClient={apollo}
            />
          )
        } 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()
      }

      // Extract query data from the Apollo store
      const apolloState = apollo.cache.extract()

      return {
        ...appProps,
        apolloState,
      }
    }

    constructor(props) {
      super(props)
      this.apolloClient = initApollo(props.apolloState)
    }

    render() {
      return <App {...this.props} apolloClient={this.apolloClient} />
    }
  }
}

These two files are boilerplate code that are taken from Next.js samples of working with Apollo and GraphQL- the first file creates a function that will connect to our backend server in two different ways depending on whether its during the pre-rendering step that occurs on the Next.js server or if it is in the user's browser.

The only change made from the original boilerplate code is that in the create HttpLink, we connect to http://localhost:4000 on the server if we are the browser but if we are in the frontend docker container we actually will connect to http://backend:4000. This is because docker-compose handles networking for us so we don't have to know what the actual IP address of the backend container is within the docker network- we can simply refer to it by a DNS name which is our container name, and docker takes care of the networking for us. Neat!

uri: isBrowser ? 'http://localhost:4000' : 'http://backend:4000',

4. Add React-Apollo to the root of the Next project.

Now that we've created the withApolloClient HOC, we can use it in the _app.tsx file. This is a special file in the pages folder which will run on every page of the Next.js site. This is exactly what we need to ensure that we have Apollo access everywhere.

Create a new file frontend/pages/_app.tsx

import App, { Container } from 'next/app'
import React from 'react'
import withApolloClient from '../utils/with-apollo-client'
import { ApolloProvider } from 'react-apollo'

class MyApp extends App {
  render() {
    // @ts-ignore
    const { Component, pageProps, apolloClient } = this.props
    return (
      <Container>
        <ApolloProvider client={apolloClient}>
          <Component {...pageProps} />
        </ApolloProvider>
      </Container>
    )
  }
}

export default withApolloClient(MyApp)

5. Install Antd for a beautiful site layout

Ant Design is a popular React UI framework that we will be using in this project. It is like Bootstrap but I think that it fits even better into the React landscape because you don't have to install jQuery for modal popups and it just generally has a look that is super clean and doesn't look like every other site out there. First we need to install it in our frontend folder:

npm install --save antd

Then we need to add the css styling to the _app.tsx file. Just add it to the bottom of the depedencies import list:

import 'antd/dist/antd.css'

In Part 3 of this blog post series we will show how to use these Ant Design components to build out all of our React Components so be sure to check back for our final installment.

Video Series for Part II:

Create Queries, Mutations and Fragments for Next.js

Add GraphQL Code Generator

Add Apollo to Next.js

Add Apollo Provider to Next.js

Add Ant Design to Next.js

Learn something new? Share it with the world!

There is more where that came from!

Drop your email in the box below and we'll let you know when we publish new stuff. We respect your email privacy, we will never spam you and you can unsubscribe anytime.