Authentication & User Management in Nextjs App Router + TypeScript, 2023

shinjithdev

shinjith

Posted on September 28, 2023

Authentication & User Management in Nextjs App Router + TypeScript, 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

  1. Setting up a nextjs project
  2. Adding basic login page
  3. Access and modify cookie using custom hook
  4. Creating AuthContext.tsx to persist user
  5. Including custom hooks for managing and modifying user
  6. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Clean up the code that was generated by nextjs
  • Our final project structure looks like this:

project structure

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • Create cookies.tsx and providers.tsx inside app directory
// /app/cookies.tsx

"use client";

import { CookiesProvider } from "next-client-cookies";

export const ClientCookiesProvider: typeof CookiesProvider = (props) => (
  <CookiesProvider {...props} />
);
Enter fullscreen mode Exit fullscreen mode
// /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>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • Add AuthProvider in providers.tsx
// /app/provider.tsx

<ClientCookiesProvider value={cookies().getAll()}>
  <AuthProvider>{children}</AuthProvider>
</ClientCookiesProvider>
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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);
    });
};
Enter fullscreen mode Exit fullscreen mode
  • 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Implementation example

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!

💖 💪 🙅 🚩
shinjithdev
shinjith

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