Production grade Next.js

Client side mutations

API routes

Rught now, our app can't even create a new folder od document. Lets fix that, first we need some API routes to create these resources, then, we need to update our components to call these endpoints.

We're going to use a package called next-connect that allows us to use connect based routing (express is based on connect). This will save us from trying to disect the request object to understand what HTTP verb a request is.

./pages/api/doc/[id],ts
import { NextApiResponse } from 'next'
import nc from 'next-connect'
import middleware from '../../../middleware/all'
import { Request } from '../../../types'
import { doc } from '../../../db'
import onError from '../../../middleware/error'

const handler = nc<Request, NextApiResponse>({
  onError,
})

handler.use(middleware)

handler.put(async (req, res) => {
  const updated = await doc.updateOne(req.db, req.query.id as string, req.body)

  res.send({ data: updated })
})

export default handler
``

Few things happening here. First, we create a handler with next-connect. Think of this handler as an express router already scoped to a route. Like with
```ts
express().route('/user')
.get()
.post()

We then add our error handler middleware and then all the middleware that was already created for you. This middleware stack handles checking the JWT and connecting to the db. We then create a controller for PUT. This will let us handle PUT requests to /api/doc/[id], basically update a doc.

Now we need a route ro handle actions agains all docs, not just one

./pages/api/doc/index.ts
import { NextApiResponse } from 'next'
import nc from 'next-connect'
import { doc } from '../../../db'
import middleware from '../../../middleware/all'
import onError from '../../../middleware/error'
import { Request } from '../../../types'

const handler = nc<Request, NextApiResponse>({
  onError,
})

handler.use(middleware)
handler.post(async (req, res) => {
  const newDoc = await doc.createDoc(req.db, {
    createdBy: req.user.id,
    folder: req.body.folder,
    name: req.body.name,
  })
  res.send({ data: newDoc })
})

export default handler

Here we create a controller to handle POST request to /api/doc that will create a doc. Now for folders.

./pages/api/folder/index.ts
import { NextApiResponse } from 'next'
import nc from 'next-connect'
import { folder } from '../../../db'
import middleware from '../../../middleware/all'
import onError from '../../../middleware/error'
import { Request } from '../../../types'

const handler = nc<Request, NextApiResponse>({
  onError,
})

handler.use(middleware)
handler.post(async (req, res) => {
  const newFolder = await folder.createFolder(req.db, { createdBy: req.user.id, name: req.body.name })
  res.send({ data: newFolder })
})

export default handler

Same with the doc, we create a controller to handle a new folder. Again, when it comes to querying the DB, we just use the DB directly in our serverSideProps, so we don't need controllers here for them. Now to hook up these mutations to our buttons in react.

Client side

We already have the buttons to create a folder and a doc. The create doc button is alrady configured. You can see it here ./components/folderPane.tsx with the handleNewDoc function. We're going to do that same for creating a new folder in the ./pages/app/[[...id]].tsx file. Because we already fetched data server sidem and now we're updated that data client side, we need to make sure we're showing the latest data in our component. There are many ways to do this using packages like swr and react-query that prepopulate the client side cahce with the results of the server side calls. We're going to keep it simple and just update our local state.

./pages/app/[[...id]].tsx
const App = () => {
    // hooks
    // local state the inits with server side state then updated
    // client side after mutations
    const [allFolders, setFolders] = useState(folders || [])
    
    const handleNewFolder = async (name: string) => {
        const res = await fetch(`${process.env.NEXT_PUBLIC_API_HOST}/api/folder/`, {
          method: 'POST',
          body: JSON.stringify({ name }),
          headers: {
            'Content-Type': 'application/json',
          },
        })
    
        const { data } = await res.json()
        // update local state
        setFolders((state) => [...state, data])
     }
 }

Next, we need to update the props we pass into <FolderList />

<FolderList folders={allFolders} />

And lastly, we need to update the onNewFolder handler for the <NewFolderDialog /> component. We will pass in our handler function that we created above

<NewFolderDialog close={() => setIsShown(false)} isShown={newFolderIsShown} onNewFolder={handleNewFolder} />

All together, your page should look like this:

./pages/app/[[...id]].tsx
import React, { FC, useState } from 'react'
import { getSession, useSession } from 'next-auth/client'
import { Pane, Dialog, majorScale } from 'evergreen-ui'
import { useRouter } from 'next/router'
import Logo from '../../components/logo'
import FolderList from '../../components/folderList'
import NewFolderButton from '../../components/newFolderButton'
import { connectToDB, folder, doc } from '../../db'
import { UserSession } from '../../types'
import User from '../../components/user'
import FolderPane from '../../components/folderPane'
import DocPane from '../../components/docPane'
import NewFolderDialog from '../../components/newFolderDialog'

const App: FC<{ folders?: any[]; activeFolder?: any; activeDoc?: any; activeDocs?: any[] }> = ({
  folders,
  activeDoc,
  activeFolder,
  activeDocs,
}) => {
  const router = useRouter()
  const [session, loading] = useSession()

  const [newFolderIsShown, setIsShown] = useState(false)
  const [allFolders, setFolders] = useState(folders || [])

  if (loading) return null

  const handleNewFolder = async (name: string) => {
    const res = await fetch(`${process.env.NEXT_PUBLIC_API_HOST}/api/folder/`, {
      method: 'POST',
      body: JSON.stringify({ name }),
      headers: {
        'Content-Type': 'application/json',
      },
    })

    const { data } = await res.json()
    setFolders((state) => [...state, data])
  }

  const Page = () => {
    if (activeDoc) {
      return <DocPane folder={activeFolder} doc={activeDoc} />
    }

    if (activeFolder) {
      return <FolderPane folder={activeFolder} docs={activeDocs} />
    }

    return null
  }

  if (!session && !loading) {
    return (
      <Dialog
        isShown
        title="Session expired"
        confirmLabel="Ok"
        hasCancel={false}
        hasClose={false}
        shouldCloseOnOverlayClick={false}
        shouldCloseOnEscapePress={false}
        onConfirm={() => router.push('/signin')}
      >
        Sign in to continue
      </Dialog>
    )
  }

  return (
    <Pane position="relative">
      <Pane width={300} position="absolute" top={0} left={0} background="tint2" height="100vh" borderRight>
        <Pane padding={majorScale(2)} display="flex" alignItems="center" justifyContent="space-between">
          <Logo />

          <NewFolderButton onClick={() => setIsShown(true)} />
        </Pane>
        <Pane>
          <FolderList folders={allFolders} />{' '}
        </Pane>
      </Pane>
      <Pane marginLeft={300} width="calc(100vw - 300px)" height="100vh" overflowY="auto" position="relative">
        <User user={session.user} />
        <Page />
      </Pane>
      <NewFolderDialog close={() => setIsShown(false)} isShown={newFolderIsShown} onNewFolder={handleNewFolder} />
    </Pane>
  )
}

export async function getServerSideProps(context) {
  const session: { user: UserSession } = await getSession(context)
  
  if (!session || !session.user) {
    return { props: {} }
  }

  const props: any = { session }
  const { db } = await connectToDB()
  const folders = await folder.getFolders(db, session.user.id)
  props.folders = folders

  if (context.params.id) {
    const activeFolder = folders.find((f) => f._id === context.params.id[0])
    const activeDocs = await doc.getDocsByFolder(db, activeFolder._id)
    props.activeFolder = activeFolder
    props.activeDocs = activeDocs

    const activeDocId = context.params.id[1]

    if (activeDocId) {
      props.activeDoc = await doc.getOneDoc(db, activeDocId)
    }
  }

  return {
    props,
  }
}

export default App

Our app, is done for this release! Congrats. Now its time to deploy!