DEV Community

loading...
Cover image for Howler | A basic fullstack Next.js App using its API routes w/ React Query

Howler | A basic fullstack Next.js App using its API routes w/ React Query

imervinc profile image 👺Mervyn ・7 min read

This is not a how to build post, but me writing down what and how I made stuff. A learning journal if you may.

The Stack

  • Next.js
  • React Query
  • TailwindCSS
  • NextAuth
  • MongoDB

Design

First of all I almost always start my projects with a design. I'm not a designer but a simple prototype helps me focus. Usually made In Figma.

Alt Text

The design is obviously inspired by twitter. Made this in Figma so that I can have a reference to follow in code as close as I can.

Setup

In this project I want to get my hands dirty with Next.js

Luckily Next.js already have a hefty amount of templates.
So I'm gonna use their with-typescript to save some time, even though adding typescript to it is pretty easy

Initializing the project

npx create-next-app --example with-typescript howler

Typesript
Now I'll just modify my tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"],
      "@/api/*": ["/pages/api/*"],

    },
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "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"
  ]
}

Enter fullscreen mode Exit fullscreen mode

I find it more helpful when learning Typescript to turn on strict mode "strict": true. This forces you to give everything typing's.

Compiler Options this is just my preference to get cleaner looking imports.
Instead of having to type this:

import Example from `../components/Example`

//or worst case.
import Example from `../../../components/Example`
Enter fullscreen mode Exit fullscreen mode

You get this! No matter where you need it.

import Example from `@/components/Example`
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS
A bit annoying at first, but fell in love with this CSS utility based framework.

npm install -D @tailwindcss/jit tailwindcss@latest postcss@latest autoprefixer@latest
Enter fullscreen mode Exit fullscreen mode
// tailwind.config.js
module.exports = {
 purge: [
    './src/pages/**/*.{js,ts,jsx,tsx}',
    './src/components/**/*.{js,ts,jsx,tsx}',
  ],
  darkMode: false,
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Post Css Config

// postcss.config.js
module.exports = {
  plugins: {
    '@tailwindcss/jit': {},
    autoprefixer: {},
  }
}
Enter fullscreen mode Exit fullscreen mode

Authentication

Implementing Open authentication in Next.js using NextAuth.js.

I'll just link their docs, It's well written!
NextAuth Docs

I will be using Github as my OAuth. Following the docs the session data you get will only include your name, email and image. But I would like to get the users github "tag" added to the session and be able to access in the frontend.

Took me awhile to figure this out but you can get the "tag" and other data from the profile parameter in the jwt callback. Like so.

API side

import NextAuth, { InitOptions } from 'next-auth'
import Providers from 'next-auth/providers'
import { NextApiRequest, NextApiResponse } from 'next/types'
import User from '@/backend/model/userModel'
import dbConnect from '@/utils/dbConnect'
import { customUser } from '@/types/Model.model'

const options: InitOptions = {
  providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
  database: process.env.MONGODB_URI,
  session: {
    jwt: true,
  },

  callbacks: {
    //Add userTag to User
    async session(session, user: customUser) {
      const sessionUser: customUser = {
        ...session.user,
        userTag: user.userTag,
        id: user.id,
      }
      return Promise.resolve({ ...session, user: sessionUser })
    },
    async jwt(token, user: customUser, profile) {
      let response = token

      if (user?.id) {
        //Connect to DataBase
        dbConnect()
        //Get User
        let dbUser = await User.findById(user.id)
        //Add UserTag if it doesn't already exist
        if (!dbUser.userTag && profile.login) {
          dbUser.userTag = profile.login
          await dbUser.save()
          console.log('No tag')
        }

        response = {
          ...token,
          id: user.id,
          userTag: dbUser.userTag,
        }
      }

      return Promise.resolve(response)
    },
  },
}

export default (req: NextApiRequest, res: NextApiResponse) =>
  NextAuth(req, res, options)

Enter fullscreen mode Exit fullscreen mode

After that, getting things works in the frontend "assuming the initial setup is done" via a hook to verify and get the session and a link to "Log in" or "Log out".

React side

import { useRouter } from 'next/router'

const Home: FC = () => {
// session - contains our user data , loading - self explanatory
  const [session, loading] = useSession()
  const route = useRouter()

// Redirects you if you are logged in
  useEffect(() => {
    session && route.push('/home')
  }, [session])

// Render if session is loading
  if (loading || session) {
    return (
      <>
        <Head>
          <title>Loading...</title>
          <link rel="icon" href="/pic1.svg" />
        </Head>
        <Loader />
      </>
    )
  }

// Render if there is no session
  return (
    <PageWarp title={'Welcome to Howler'} splash>
      <LoginPage />
    </PageWarp>
  )
}

export default Home

Enter fullscreen mode Exit fullscreen mode

State Management

Using React Context API for application global state to keep track
of states like dark mode or navigation , and used React Query to keep asynchronous data in cache.

Debated using Redux but changed my mind when I heard about SWR and React Query. Ended up using React Query because it has a dev tool that allows you to peek on what data is being cached.

React Query
So this is how it goes.

Like a global state, we have to wrap it our entire app. With the QueryClientProvider and this prop client={queryClient}. Imported from "react-query".

While I'm at it, also add the dev tools overlay


import { QueryClientProvider, QueryClient } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'

//React Query Connection
const queryClient = new QueryClient()

const QState: FC = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>
        {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

export default QState

Enter fullscreen mode Exit fullscreen mode

Then we can wrap that around our global state provider.
React Context


import React, { FC, useReducer, createContext } from 'react'
import { InitialHowlState, HowlReducer, howlNav } from '@/types/Howl.model'

import QState from @/components/context/QState

// Create Context
const HowlCtx = createContext<HowlContext>({} as HowlContext)

//Reducer
const howlReducer: HowlReducer = (state, action): InitialHowlState => {
  switch (action.type) {
    //Navigation State
    case 'NAVIGATION':
      return { ...state, nav: action.payload }
    default:
      return state
  }
}

//INITIAL STATE
const initialState: InitialHowlState = {
  nav: 'home',
}

const HowlState: FC = ({ children }) => {
  const [state, dispatch] = useReducer<HowlReducer>(howlReducer, initialState)

  //ACTIONS
  const setNavigation = (nav: howlNav) => {
    dispatch({ type: 'NAVIGATION', payload: nav })
  }

  return (
    <QState >
      <HowlCtx.Provider value={{ state, setNavigation }}>
        {children}
      </HowlCtx.Provider>
    </QState >
  )
}

export default HowlState

Enter fullscreen mode Exit fullscreen mode

Using React Query

Reminder React Query does not replace FETCH API or AXIOS

Fetching Data in React query we use a hook useQuery. It goes like this.

import { useQuery } from 'react-query'
import axios from 'axios'

const App = () => {
const fetcher = async (_url: string) => {
  const { data } = await axios.get(_url)
  return data
}

  // First argument Naming the data to be cached | Second argument your fetcher. Where your fetch api goes. 
   const { isLoading, isError, data, error } = useQuery('name', fetcher('https://api.example'))
 }
Enter fullscreen mode Exit fullscreen mode

More Info in thier docs.

I'll just make a bunch of these as a custom hooks. So you can use them repeatedly.

Typings on useQuery hooks are just like react hooks 'Generics'

import { useQuery } from 'react-query'
import axios from 'axios'
import { HowlT, HowlUser } from '@/types/Howl.model'

export const fetcher = async (_url: string) => {
  const { data } = await axios.get(_url)
  return data
}

export const useGetHowls = (options?: UseQueryOptions<HowlT[]>) => {
  return useQuery<HowlT[]>('howls', () => fetcher('/api/howl'), options)
}

export const useGetHowlById = (_id: string) => {
  return useQuery<HowlT>(['howls', _id], () => fetcher(`/api/howl/${_id}`), {
    enabled: false,
  })
Enter fullscreen mode Exit fullscreen mode

Usage just like any other hooks

import { useGetHowls } from '@/hooks/queryHooks'

const App = () => {
 const { data, isLoading } = useGetHowls()

 return(
  <div>
   {data?.map((howl) => <Howl {...howl}/> )}
  </div>
 )
}
Enter fullscreen mode Exit fullscreen mode

For Updating, Deleting, or Creating posts we will need to use useMutation and making a custom hook for this too. Better explained in their docs. useMutation

First argument should be your fetch function and Second is an object of side effects.

Example below shows a post request with an onSucess side effect that triggers on request success. I made the new posted howl append to the existing cached data setQueryData and invalidate invalidateQueries it to get the latest data.

export const useCreateHowl = () => {
  const queryClient = useQueryClient() 
  return useMutation(
    (newHowl: { howl: string }) => axios.post('/api/howl', newHowl),
    {
      onSuccess: (data) => {
        queryClient.setQueryData<HowlT[]>('howls', (old) => [
          data.data,
          ...old!,
        ])
        // console.log(data)
        queryClient.invalidateQueries('howls')
      },
    }
  )
}
Enter fullscreen mode Exit fullscreen mode

You can also do more optimistic update if your confident on your api, use onMutate side effect, where you manipulate the data even before getting the result from your request either successful or not.

"A" in JAM stack! REST API

Next API Routes
I'll be using next-connect package to mimic Express App syntax instead of using switch.

Before

export default function handler(req, res) {
  switch (method) {
    case 'GET':
      // Get data from your database
      break
    case 'PUT':
      // Update or create data in your database
      break
    default:
     return
  }
}
Enter fullscreen mode Exit fullscreen mode

After

Create a middleware first. Passing in your database connection function to get access to it when using this middleware


import dbMiddleware from './db'
import nextConnect from 'next-connect'

export default function createHandler(...middlewares: any[]) {
                          //Connect to DB
  return nextConnect().use(dbMiddleware, ...middlewares)
}
Enter fullscreen mode Exit fullscreen mode
//API Route
import createHandler from '@/backend/middleware'
//protect is a middleware I made for verifying session login with NextAuth.js
import { protect } from '@/backend/middleware/protect'
import { addHowl, getHowls } from '@/backend/controller/howlController'

const handler = createHandler()

handler.get(getHowls)
handler.post(protect, addHowl)

export default handler

Enter fullscreen mode Exit fullscreen mode

I can also follow MVC design pattern with this like an Express App does, so my API can be more modular.

Controllers looks like this. With comments as a reminder of what they do.

//@desc   Get Howls
//@route  GET /api/howl
//@access Public
export const getHowls = async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    const howls = await Howl.find({})
      .populate('user', 'name image userTag')
      .sort({ createdAt: -1 })
    return res.status(200).json(howls)
  } catch (error) {
    res.status(404)
    throw new Error('Error! No howls found')
  }
}
Enter fullscreen mode Exit fullscreen mode

Icing in the cake

What's a personal project without some fancy animation?

For most of my project in react I always use Framer Motion. Easy to get started with simple animation like entrance animation or page transition, and you can always up your game with this complex animation framework.

Howler Preview

New Features?

  • Uploading photos. Maybe using AWS S3 bucket or Firestore
  • Comments
  • Follow Users

Conclusion

Typescript is awesome🦾 The main hook for TS, is that prevents bugs right in your dev environment, but I like the hinting's more!

React Query is mind-blowing💥 Changes your way of thinking about organizing your global state. Separating your local state and asynchronous make freaking sense!

Next.js is just the 💣 Can't imagine doing react with vanilla create react app anymore. And deploying it in Vercel is just smooth, CICD for someone like me who just want their project to be out there!

Still have A lot more to learn, but I'm having fun!
Alt Text

LINKS

Github Repo
Say Hi! in the Live Demo

That is all! Arrivederci!

Alt Text

Discussion (2)

pic
Editor guide
Collapse
katt profile image
Alex Johansson • Edited

Try adding trpc.io into the mix and you'll get those Howl types & axios calls on the front-end automatically without any extra work

Collapse
imervinc profile image
👺Mervyn Author

Will check this out! Thanks!