Authentication & User Management in Nextjs App Router + TypeScript, 2023
shinjith
Posted on September 28, 2023
Setting up authentication on a react project with typescript is a difficult task. With the new Nextjs app router version, it is much more difficult to manage storage and cookies because it has gotten more complex and server-centric.
I'm here to share some code snippets for basic authentication and user management in Nextjs (>13) + typescript.
Prerequisite
- Basic understanding of javascript and typescript
- Basic knowledge of Nextjs/React
Steps involved
- Setting up a nextjs project
- Adding basic login page
- Access and modify cookie using custom hook
- Creating
AuthContext.tsx
to persist user - Including custom hooks for managing and modifying user
- Implementation inside components
Setting up a nextjs project
We will be using the app router version of nextjs, which was recently released. Have a look on this page. It includes neccessary details and some FAQs.
- Enter this command and answer the promt question to setup next project:
npx create-next-app@latest
Note: While answering, try to use recommended/default options.
Check this page for customized manual installations.
- Install required packages
cd next13-auth # Use your project-name instead
rm package-lock.json
yarn
yarn add axios formik next-client-cookies
I prefer using yarn over npm cli. However, you don't have to remove
package.json
if you are using npm commands.
- Start server by running
yarn dev
- Clean up the code that was generated by nextjs
- Our final project structure looks like this:
Note: Refer docs for better undertanding of recommended nextjs project structure.
Adding basic login page
- Add required type declaration for authentication
// /utils/types/auth.d.ts
export type TUser = {
email: string;
firstName: string;
lastName: string;
};
export type AuthUser = {
token: string;
user: TUser;
};
export type TLogin = {
email: string;
password: string;
};
export type AuthResponse = {
message: string;
data?: AuthUser;
success?: boolean;
};
Note: You might have to add/remove and structure types according to your needs and response format from backend!
- Add login page to your app directory.
// /app/login/page.tsx
"use client";
import { TLogin } from "@/utils/types/auth";
import { Field, Form, Formik, FormikHelpers } from "formik";
import React from "react";
const Login = () => {
const handleSubmit = (values: TLogin) => {
console.log(values);
};
return (
<div className="max-w-[100vw] p-5">
<h3 className="mb-5 text-4xl font-medium">Login</h3>
<Formik
initialValues={{
email: "",
password: "",
}}
onSubmit={(
values: TLogin,
{ setSubmitting }: FormikHelpers<TLogin>
) => {
setTimeout(() => {
handleSubmit(values);
setSubmitting(false);
}, 500);
}}
>
<Form className="grid w-96 grid-cols-2 gap-3">
<label htmlFor="email">Email</label>
<Field
id="email"
name="email"
placeholder="Enter email"
type="email"
/>
<label htmlFor="password">Password</label>
<Field
id="password"
name="password"
type="password"
placeholder="Enter password"
/>
<button
type="submit"
className="w-fit border border-black/75 px-4 py-1"
>
Submit
</button>
</Form>
</Formik>
</div>
);
};
export default Login;
Note: I will only walk you through creating a login page; you should create/use a backend that can handle basic authentication requests. Also, populate your database with a user to test login action.
Access and modify cookie using custom hook
- Create a custom hook
useCookie.ts
to modify cookie values
// /hooks/useCookie.ts
import { useCookies } from "next-client-cookies";
const useCookie = () => {
const cookies = useCookies();
const getCookie = (key: string) => cookies.get(key);
const setCookie = (key: string, value: string) =>
cookies.set(key, value, {
expires: 2,
sameSite: "None",
secure: true,
});
const removeCookie = (key: string) => cookies.remove(key);
return { setCookie, getCookie, removeCookie };
};
export default useCookie;
- Create
cookies.tsx
andproviders.tsx
insideapp
directory
// /app/cookies.tsx
"use client";
import { CookiesProvider } from "next-client-cookies";
export const ClientCookiesProvider: typeof CookiesProvider = (props) => (
<CookiesProvider {...props} />
);
// /app/providers.tsx
import { ClientCookiesProvider } from "./cookies";
import { cookies } from "next/headers";
export function Providers({ children }: React.PropsWithChildren) {
return (
<ClientCookiesProvider value={cookies().getAll()}>
{children}
</ClientCookiesProvider>
);
}
- Add
provider.tsx
as a wrapper inside layout
// /app/layout.tsx
<body className={`${outfit.className} relative min-h-screen w-screen`}>
<Providers>
<main className="w-screen">{children}</main>
</Providers>
</body>
Refer this readme for detailed explaination.
Creating AuthContext.tsx
to persist user
- Create
AuthContext.tsx
// /contexts/AuthContext.tsx
"use client";
import { ReactNode, createContext, useEffect, useState } from "react";
import { AuthUser } from "@/utils/types/auth";
import useCookie from "@/hooks/useCookie";
interface TAuthContext {
user: AuthUser | null;
setUser: (user: AuthUser | null) => void;
}
export const AuthContext = createContext<TAuthContext>({
user: null,
setUser: () => {},
});
interface Props {
children: ReactNode;
}
export const AuthProvider = ({ children }: Props) => {
const [user, setUser] = useState<AuthUser | null>(null);
const { getCookie } = useCookie();
useEffect(() => {
if (!user) {
let existingUser = null;
const getFromCookie = async () => (existingUser = getCookie("user"));
getFromCookie();
if (existingUser) {
try {
setUser(JSON.parse(existingUser));
} catch (e) {
console.log(e);
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
};
- Add
AuthProvider
inproviders.tsx
// /app/provider.tsx
<ClientCookiesProvider value={cookies().getAll()}>
<AuthProvider>{children}</AuthProvider>
</ClientCookiesProvider>
Including custom hooks for managing and modifying user
This custom hooks are basically an extension that makes accessing and modifying user in authentication and other components easier without using cookies methods directly.
- Add
useAuth.ts
to manage auth API requests and their response
// /hooks/useAuth.ts
import { useUser } from "./useUser";
import config from "@/utils/config";
import axios from "axios";
import { AuthResponse, TLogin, TRegister } from "@/utils/types/auth";
import useCookie from "./useCookie";
const API_URL = config.BACKEND_URL;
export const useAuth = () => {
const { user, addUser, removeUser } = useUser();
const { getCookie } = useCookie();
const refresh = () => {
let existingUser = null;
const getFromCookie = async () => (existingUser = getCookie("user"));
getFromCookie();
if (existingUser) {
try {
addUser(JSON.parse(existingUser));
} catch (e) {
console.log(e);
}
}
};
const register = async (creds: TRegister) => {
return await axios
.post(`${API_URL}auth/register`, creds)
.then((res) => {
if (res.data?.data && res.data.data?.token) addUser(res.data.data);
return res.data as AuthResponse;
})
.catch((err) => {
if (err && err?.response && err.response?.data)
return { ...err.response.data, success: false } as AuthResponse;
else return err as AuthResponse;
});
};
const login = async (creds: TLogin) => {
return await axios
.post(`${API_URL}auth/login`, creds)
.then((res) => {
if (res.data?.data && res.data.data?.token) addUser(res.data.data);
return res.data as AuthResponse;
})
.catch((err) => {
if (err && err?.response && err.response?.data)
return { ...err.response.data, success: false } as AuthResponse;
else return err as AuthResponse;
});
};
const logout = () => {
removeUser();
};
return { user, login, register, logout, refresh };
};
Update authentication requests and response handling according to you backend.
- Add
useUser.ts
to export functions to add/remove user
// /hooks/useUser.ts
import { useContext } from "react";
import { AuthContext } from "../contexts/AuthContext";
import { AuthUser } from "@/utils/types/auth";
import useCookie from "./useCookie";
export const useUser = () => {
const { user, setUser } = useContext(AuthContext);
const { setCookie, removeCookie } = useCookie();
const addUser = (user: AuthUser) => {
setUser(user);
setCookie("user", JSON.stringify(user));
};
const removeUser = () => {
setUser(null);
removeCookie("user");
};
return { user, addUser, removeUser };
};
Implementation inside components
- Update your login submit handler function with login auth function
// /app/login/page.tsx
const handleSubmit = (values: TLogin) => {
console.log(values);
login(values)
.then((data) => {
if (data?.success) {
// add your code for post successful login here
setTimeout(() => {
router.push("/");
}, 1000);
} else console.log(data.message);
})
.catch((err) => {
console.log(err);
});
};
- Finally, use this value across your components
example:
// /app/page.tsx
"use client";
import { useUser } from "@/hooks/useUser";
import Link from "next/link";
export default function Home() {
const { user } = useUser();
return (
<div className="p-5">
<h2 className="text-3xl font-medium">Hello Nextjs</h2>
<p className="my-5 text-sm font-mono">
Cookie-user: <pre>{JSON.stringify(user, undefined, 4)}</pre>
</p>
<Link
href={"/login"}
className="py-1 px-4 border border-black/75"
>
Login
</Link>
</div>
);
}
Source code: nextjs-auth
Conclusion
I tried to make it as broad as possible. Changes and upgrades should be made based on your use cases.
I couldn't find any helpful blogs on this topic, so I'm hoping this can help someone who is looking for something similar. Please feel free to make any recommendations. Thank you for your time!
Posted on September 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 28, 2023