Next.js (Part 3)
Neil Haddley • November 7, 2021
API routes and next-auth
Get Request
I used API routes to create API endpoints inside a Next.js app. I added a file to the /pages/api directory to create each endpoint.

Get Request
Dynamic API routes
I made API routes dynamic, just like regular Next.js pages.

Dynamic API Route
Unprotected pages
Some pages didn't require login to access.

Unprotected page
next-auth
I used next-auth to prevent unauthorized access to protected API methods and pages. I installed it with npm install next-auth.
GitHub Id and GitHub Secret
Here I used GitHub as an authentication provider.

GitHub Developer Settings

Register a new OAuth application

Note the Client ID and Client Secret
.env
I stored the Client ID and Client Secret values in an .env file.
[...nextauth]
I added a [...nextauth] API method.
_app.js
I added a next-auth/client Provider to _app.js.

Provider
Add code to prevent unauthorized access
I checked that a valid session existed before returning articles or article details, whether via a REST API call or a web page request.

/api/articles is now protected

/api/articles/4 is now protected

page /protected is now protected (server-side)

page /protected is now protected (client-side)
Adding login and logout to home page
I added "Sign In" and "Sign Out" buttons to the home page.

Sign in

Sign in with GitHub provider

Signed in

authorized

authorized

authorized
next-auth database (optional)
Specifying a database is optional — if I didn't need to persist user data or support email sign-in, JSON Web Tokens would be used for session storage instead. To specify a database, I updated the [...nextauth].js file and the relevant environment variables.

accounts

users

session (with user id and provider)

pages
articles.js and pagesapiarticlesindex.js
JAVASCRIPT
1// articles.js 2 3export const articles = [ 4 { 5 "userId": 1, 6 "id": 1, 7 "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", 8 "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" 9 }, 10 { 11 "userId": 1, 12 "id": 2, 13 "title": "qui est esse", 14 "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" 15 } 16] 17 18// /pages/api/articles/index.js 19import { articles } from '../../../articles' 20 21const index = async (req, res) => { 22 23 res.status(200).json(articles) 24 25} 26 27export default index
pagesapiarticlesid.js
JAVASCRIPT
1// /pages/api/articles/[id].js 2import {articles} from '../../../articles' 3 4const article = async (req, res) => { 5 6 const { query: { id } } = req 7 8 const article = articles.find(a => a.id.toString() === id) 9 10 if (!article) { 11 res.status(404).json({ message: `article ${id} not found` }) 12 } 13 14 res.status(200).json(article) 15 16} 17 18export default article
pagesunprotected
JAVASCRIPT
1import { articles } from '../articles' 2 3function index() { 4 5 return ( 6 <div> 7 <ul> 8 {articles.map(article => (<li key={article.id}>{article.title}</li>))} 9 </ul> 10 </div> 11 ) 12} 13 14export default index
.env
TEXT
1//.env 2NEXTAUTH_GITHUB_ID=e95a2816a93e7daabc6c 3NEXTAUTH_GITHUB_SECRET=<secret>
apiauth...nextauth.js
JAVASCRIPT
1// /api/auth/[...nextauth].js 2import NextAuth from "next-auth"; 3import Providers from "next-auth/providers"; 4 5const options = { 6 // @link https://next-auth.js.org/configuration/providers 7 providers: [ 8 Providers.GitHub({ 9 clientId: process.env.NEXTAUTH_GITHUB_ID, 10 clientSecret: process.env.NEXTAUTH_GITHUB_SECRET, 11 }), 12 /*Providers.Google({ 13 clientId: process.env.NEXTAUTH_GOOGLE_ID, 14 clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET, 15 }), 16 Providers.Facebook({ 17 clientId: process.env.NEXTAUTH_FACEBOOK_ID, 18 clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET, 19 }),*/ 20 ] 21} 22 23export default (req,res) => NextAuth(req,res,options)
_app.js
JAVASCRIPT
1import { createGlobalStyle, ThemeProvider } from 'styled-components' 2import { Provider } from "next-auth/client"; 3 4const GlobalStyle = createGlobalStyle` 5 body { 6 margin: 0; 7 padding: 0; 8 box-sizing: border-box; 9 } 10` 11 12const theme = { 13 colors: { 14 primary: '#0070f3', 15 }, 16} 17 18export default function App({ Component, pageProps }) { 19 return ( 20 <> 21 <Provider session={pageProps.session}> 22 <GlobalStyle /> 23 <ThemeProvider theme={theme}> 24 <Component {...pageProps} /> 25 </ThemeProvider> 26 </Provider> 27 </> 28 ) 29}
pagesapiarticlesindex.js updated
JAVASCRIPT
1import { articles } from '../../../articles' 2import { getSession } from 'next-auth/client' 3 4const index = async (req, res) => { 5 6 const session = await getSession({ req }); 7 if (session) { 8 res.status(200).json(articles) 9 } else { 10 res.status(401).json({ message: `please log in` }) 11 } 12 13} 14 15export default index
pagesapiarticlesid.js updated
JAVASCRIPT
1import { articles } from '../../../articles' 2import { getSession } from 'next-auth/client' 3 4const article = async (req, res) => { 5 6 const session = await getSession({ req }); 7 if (session) { 8 const { query: { id } } = req 9 const article = articles.find(a => a.id.toString() === id) 10 if (!article) { 11 res.status(404).json({ message: `article ${id} not found` }) 12 } 13 res.status(200).json(article) 14 } else { 15 res.status(401).json({ message: `please log in` }) 16 } 17 18} 19 20export default article
pagesprotected.js
JAVASCRIPT
1import { getSession } from "next-auth/client" 2import { articles } from '../articles' 3 4function index({ session, articles }) { 5 6 // If no session exists, display access denied message 7 if (!session) { return (<div>You need to be logged on</div>) } 8 9 return ( 10 <div> 11 <ul> 12 {articles.map(article => (<li key={article.id}>{article.title}</li>))} 13 </ul> 14 </div> 15 ) 16} 17 18export default index 19 20export const getServerSideProps = async (context) => { 21 22 const session = await getSession(context); 23 24 // If no session exists, display access denied message 25 if (!session) { 26 return { 27 props: { 28 session 29 } 30 } 31 } 32 33 return { 34 props: { 35 session, 36 articles 37 } 38 } 39}
pagesprotected.js
JAVASCRIPT
1import { useSession } from "next-auth/client" 2import useSWR from 'swr' 3 4function index() { 5 6 const [ session, loading ] = useSession() 7 8 // When rendering client side don't display anything until loading is complete 9 if (typeof window !== 'undefined' && loading) return null 10 11 // If no session exists, display access denied message 12 if (!session) { return (<div>You need to be logged on</div>) } 13 14 15 const fetcher = (...args) => fetch(...args).then(res => res.json()) 16 17 const { data, error } = useSWR('/api/articles', fetcher) 18 19 if (error) return <div>failed to load</div> 20 if (!data) return <div>loading...</div> 21 22 return ( 23 <div> 24 <ul> 25 {data.map(article => (<li key={article.id}>{article.title}</li>))} 26 </ul> 27 </div> 28 ) 29 30} 31 32export default index
pagesindex.js
JAVASCRIPT
1import { signIn, signOut, useSession } from 'next-auth/client' 2 3export default function Home() { 4 const [session, loading] = useSession() 5 6 return ( 7 <> 8 {!session && (<> 9 Not signed in 10 <br /> 11 <button onClick={signIn}>Sign In</button> 12 </>)} 13 {session && (<> 14 Signed in as {session.user.email} 15 <br /> 16 <button onClick={signOut}>Sign Out</button> 17 </>)} 18 </> 19 ) 20}
...nextauth.js
JAVASCRIPT
1import NextAuth from "next-auth"; 2import { session } from "next-auth/client"; 3import Providers from "next-auth/providers"; 4 5const options = { 6 // @link https://next-auth.js.org/configuration/providers 7 providers: [ 8 Providers.GitHub({ 9 clientId: process.env.NEXTAUTH_GITHUB_ID, 10 clientSecret: process.env.NEXTAUTH_GITHUB_SECRET, 11 }), 12 /*Providers.Google({ 13 clientId: process.env.NEXTAUTH_GOOGLE_ID, 14 clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET, 15 }), 16 Providers.Facebook({ 17 clientId: process.env.NEXTAUTH_FACEBOOK_ID, 18 clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET, 19 }),*/ 20 ], 21 database: process.env.DB_URL, 22 session: { 23 jwt: true 24 }, 25 jwt: { 26 secret: process.env.JWT_SECRET 27 }, 28 callbacks: { 29 async jwt(token, user, account, profile, isNewUser) { 30 console.log("token", token) 31 console.log("user", user) 32 console.log("account", account) 33 console.log("profile", profile) 34 console.log("isNewUser", isNewUser) 35 if (user && user.id) { 36 token.id = user.id 37 } 38 if (account && account.provider) { 39 token.provider = account.provider 40 } 41 return token 42 }, 43 async session(session, token) { 44 session.user.id = token.id 45 session.user.provider = token.provider 46 return session 47 } 48 } 49} 50 51export default (req, res) => NextAuth(req, res, options)
.env
TEXT
1NEXTAUTH_GITHUB_ID=e95a2816a93e7daabc6c 2NEXTAUTH_GITHUB_SECRET=<secret> 3 4DB_USER=<user> 5DB_PASSWORD=<password> 6DB_URL=mongodb+srv://$DB_USER:$DB_PASSWORD@cluster0.gdnd5.mongodb.net/nextauthDB?retryWrites=true&w=majority 7JWT_SECRET=<secret> 8NEXTAUTH_URL=http://localhost:3000