Howler | A basic fullstack Next.js App using its API routes w/ React Query
👺Mervyn
Posted on March 28, 2021
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.
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"
]
}
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`
You get this! No matter where you need it.
import Example from `@/components/Example`
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
// 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: [],
}
Post Css Config
// postcss.config.js
module.exports = {
plugins: {
'@tailwindcss/jit': {},
autoprefixer: {},
}
}
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)
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
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
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
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'))
}
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,
})
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>
)
}
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')
},
}
)
}
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
}
}
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)
}
//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
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')
}
}
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.
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!
LINKS
Github Repo
Say Hi! in the Live Demo
That is all! Arrivederci!
Posted on March 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.