GET and POST with Next.js SWR and Supabase Auth

Simple workflow for fetching and storing data from the Supabase database using the SWR fetching library for authenticated users.

Markus Tripp
4 min readApr 29, 2022

Next.js and Supabase are fantastic technologies. With Supabase, you get a real database in the cloud. But best of all, it comes with authentication, managing user data, row-level security, database functions, and storage built-in. With Supabase you can combine all those tools, which reduces complexity significantly.

This article outlines a simplified application setup using Next.js, SWR data fetching, and Supabase. The app requires that every user is logged in. Then you can make authenticated GET and POST calls on your pages very easily.

File: ./pages/tasks.jsconst Tasks = () => {
const { fetcher, mutate } = useSWRConfig()
const { data: tasks } = useSWR('/api/tasks')
const [add, setAdd] = useState({})
...
const handleAdd = async () => {
await fetcher(url, add)
mutate('/api/tasks')
}
...
}

On the API level you can then make your authenticated calls to Supabase:

File: ./pages/api/tasks.jsconst Tasks = async (req, res) => {
const token = req.headers.token
const jwt = jwt_decode(token)
supabase.auth.setAuth(token)
if (req.method === 'GET') {
return getTasks(res)
} else if (req.method === 'POST') {
return saveTask(res, req.body, jwt)
}
}

Custom App

Next.js uses the App component to initialize pages. This is where you add the user session management and SWR configuration.

File: ./pages/_app.jsimport { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
import { SWRConfig } from 'swr'
import { supabase } from '../lib/supabase'
import Auth from '../components/Auth'
import '../styles/globals.css'
const MyApp = ({ Component, pageProps }) => {
const router = useRouter()
const [session, setSession] = useState(null)
useEffect(() => {
if (!router.asPath.includes('#access_token')) {
sessionStorage.removeItem('AUTH')
}
setSession(supabase.auth.session())
const { data: authListener } = supabase.auth.onAuthStateChange(
(_event, session) => { setSession(session) }
)
const handleRouteChange = () => setError(false) router.events.on('routeChangeStart', handleRouteChange) return () => {
authListener.unsubscribe()
router.events.off('routeChangeStart')
}
}, [router])
const fetcher = (url, data) => {
const options = {
headers: new Headers({
'Content-Type': 'application/json',
token: session.access_token,
}),
credentials: 'same-origin',
}
if (data) {
options.method = 'POST'
options.body = JSON.stringify(data)
}
return fetch(url, options).then((res) => {
if (!res.ok) {
// global error handling
}
return res.json()
})
}
return session ? (
<SWRConfig value={{ fetcher }}>
<Component {...pageProps} />
</SWRConfig>
) : (
<Auth />
)
}
export default MyApp

Authentication and Session Management

return session ? (
<SWRConfig value={{ fetcher }}>
<Component {...pageProps} />
</SWRConfig>
) : (
<Auth />
)

If the user has no valid session, the <Auth />component with the options for log-in will be displayed.

Find more information on how to set up Supabase Auth with Next.js here.

SWR Custom Fetcher

In this example, I created a custom fetcher for SWR. On every request, I add the Supabase access_tokento the header.

const options = {
headers: new Headers({
'Content-Type': 'application/json',
token: session.access_token,
}),
credentials: 'same-origin',
}

In addition, the custom fetcher function accepts a data object. When you add a data object, I assume you want to make a POST request to the server.

if (data) {
options.method = 'POST'
options.body = JSON.stringify(data)
}

In the app, I have access to the fetcher and can simply make the following calls:

const Tasks = () => {
const { fetcher, mutate } = useSWRConfig()
const { data: tasks } = useSWR('/api/tasks')
const [add, setAdd] = useState({})
...
const handleAdd = async () => {
await fetcher(url, add)
mutate('/api/tasks')
}
...
}

Task Page

Here is a full example of the task page:

File: ./pages/tasks.jsimport { useState } from 'react'
import useSWR, { useSWRConfig } from 'swr'
import Layout from '../components/Layout'
import Tasks from '../components/Tasks'
import AddTask from '../components/AddTask'
const url = '/api/tasks'const Tasks = () => {
const { fetcher, mutate } = useSWRConfig()
const { data: tasks } = useSWR(url)
const [add, setAdd] = useState({
name: '',
status: 'open',
})
const handleAdd = async () => {
await fetcher(url, add)
mutate(url)
}
return (
<Layout>
<Tasks tasks={tasks} />
<AddTask
add={add}
setAdd={setAdd}
handleAdd={handleAdd} />
</Layout>
)
}
export default Tasks

Database Calls via API

I prefer to make the database calls from the server. As the access_tokenis added to every request, it is easy to perform authenticated requests via the Supabase REST API.

File: ./pages/api/tasks.jsimport { supabase } from '../lib/supabase'
import jwt_decode from 'jwt-decode'
const Tasks = async (req, res) => {
const token = req.headers.token
const jwt = jwt_decode(token)
supabase.auth.setAuth(token)
if (req.method === 'GET') {
return getTasks(res)
} else if (req.method === 'POST') {
return saveTask(res, req.body, jwt)
}
return res.status(404).json({ error: 'API method not found' })
}
const getTasks = async (res) => {
const { data, error } = await supabase.from('tasks').select('*')
if (error) return res.status(500).json({ error: error.message })
return res.status(200).json(data)
}
const saveTask = async (res, body, jwt) => {
const input = { ...body, created_by: jwt.sub }
const { data, error } = await supabase
.from('tasks')
.upsert([input])
if (error) return res.status(500).json({ error: error.message })
return res.status(200).json(data)
}
export default Tasks

Conclusion

You can create a slim, secure, and high-performance web application using this application setup. I love it!

About the author (Markus Tripp):
I’m a freelance web developer and Shopify consultant — and I’m the creator of Headcode CMS (www.headcodecms.com), a 100% open-source headless CMS for Next.js 13 App Router, Server Components, and Server Actions. Watch the video below for a quick product demo:

--

--