Production grade Next.js

Server side data fetching

DB CRUD

We need to get all our data for our app on the server before we render that app page. Our app has two data models that we're interested in:

  • folder - organizes our docs
  • doc - a place where we can take notes. Belongs to a folder

Lets create some helper functions using the DB that we can then reuse in our App page server side at ./db/folder.ts

./db/folder.ts
import { Db } from 'mongodb'
import { nanoid } from 'nanoid'

export const createFolder = async (db: Db, folder: { createdBy: string; name: string }) => {
  return db
    .collection('folders')
    .insertOne({
      _id: nanoid(12),
      ...folder,
      createdAt: new Date().toDateString(),
    })
    .then(({ ops }) => ops[0])
}

export const getFolders = async (db: Db, userId: string) => {
  return db
    .collection('folders')
    .find({
      createdBy: userId,
    })
    .toArray()
}

Nothing fancy here, just helpr methods using the db instance to operate on our Mongodb. If you're new to mongo, its fine, just follow along here. Important to note that becuase we're setting custom _id's ourselves, but we don't have to as mongodb will do that using an ObjectId.

Next, lets do the same for docs for ./db/doc.ts

./db/doc.ts
import { Db } from 'mongodb'
import { nanoid } from 'nanoid'

export const getOneDoc = async (db: Db, id: string) => {
  return db.collection('docs').findOne({ _id: id })
}

export const getDocsByFolder = async (db: Db, folderId: string) => {
  return db.collection('docs').find({ folder: folderId }).toArray()
}

export const createDoc = async (db: Db, doc: { createdBy: string; folder: string; name: string; content?: any }) => {
  return db
    .collection('docs')
    .insertOne({
      _id: nanoid(12),
      ...doc,
      createdAt: new Date().toDateString(),
    })
    .then(({ ops }) => ops[0])
}

export const updateOne = async (db: Db, id: string, updates: any) => {
  const operation = await db.collection('docs').updateOne(
    {
      _id: id,
    },
    { $set: updates },
  )

  if (!operation.result.ok) {
    throw new Error('Could not update document')
  }

  const updated = await db.collection('docs').findOne({ _id: id })
  return updated
}

Again, simple CRUD functions to operate on our docs collection on MongoDB. Lastly, users.

./db/user.ts
import { Db } from 'mongodb'

export const getUserById = async (db: Db, id: string) => {
  return db.collection('users').findOne({ _id: id })
}

The user collection is already created for us by next-auth. We just have a helper function here to be able to get a user by id.

Server side props

Now we can uses these functions on pur app page server side to actually fetch data. This might be new to you. Typically, we'd hit our API that would then call these functions, however, Next.js recommends against this for some good reasons. One being, why go through the network to call your own API we yuo can access server side tools and logic directly in yuor page without network overhead? This leads to a pattern of creating API routes for either mutations and or to be consumed by another app. DB reads can be used in your pages. Thats the pattern we're using in our app, Known. Now, lets fetch our folders and docs for our signed in user.

./pages/app/[[,,,id]].tsx
import { connectToDB, folder, doc } from '../../db'

// at the bottom
export async function getServerSideProps(context) {
  const session: { user: UserSession } = await getSession(context)
  // not signed in
  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,
  }
}

Here, we import those DB helper functions we just created! I know, crazy, right in our React component. Then we update our server side props handler to fetch the data. This function is a bit complicated because of a design choice of using one optional catch all route. Because of that, our server side props function has to handle each scenario for this route. A different way would be to make another page for each scenario, one for viewing all folders, another for one folder selected, and one more for a selected doc. I chose to make one route because they're all the same JSX really and wanted to flex the power of serverSideProps. We shuold now be able to visit /app and see some data. Except there is none, lets fix that by creating some getting started data for a new user.

./pages/api/[...nextauth].tsimport

// on the NextAuth config

callbacks: {
  async session(session, user) {
    session.user.id = user.id
    return session
  },
  async jwt(tokenPayload, user, account, profile, isNewUser) {
    const { db } = await connectToDB()

    if (isNewUser) {
      const personalFolder = await folder.createFolder(db, { createdBy: `${user.id}`, name: 'Getting Started' })
      await doc.createDoc(db, {
        name: 'Start Here',
        folder: personalFolder._id,
        createdBy: `${user.id}`,
        content: {
          time: 1556098174501,
          blocks: [
            {
              type: 'header',
              data: {
                text: 'Some default content',
                level: 2,
              },
            },
          ],
          version: '2.12.4',
        },
      })
    }

    if (tokenPayload && user) {
      return { ...tokenPayload, id: `${user.id}` }
    }

    return tokenPayload
  },
},

next-auth allows us to tap into certain phases of auth with callbacks. So we created two callbacks. One for adding the user id to the session, and one for creating a gettting started folder and doc for new users. We're using the db functions we created earlier. You might have to remove your self from the DB, or just drop the DB and signin again. Might also have to clear your brower cookes for this localhost to the app to logout. Signup again with github and you should have some seed data! Lets hook up our client side mutations to create docs and folders.