Authentication & User Management in Nextjs App Router + TypeScript, 2023
Tags: typescript, react, nextjs, javascript
Published: 2023-09-28 05:02:44

Setting up authentication on a react project with typescript is a difficult task. With the new [Nextjs app router](https://nextjs.org/docs#app-router-vs-pages-router) version, it is much more difficult to manage storage and cookies because it has gotten more complex and server-centric.<br/ >

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
<hr />

## 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](https://nextjs.org/docs/app). It includes neccessary details and some FAQs.

- Enter this command and answer the promt question to setup next project:

``` sh
npx create-next-app@latest
```

> Note: While answering, try to use recommended/default options.

Check [this page](https://nextjs.org/docs/getting-started/installation#manual-installation) for customized manual installations.

- Install required packages

``` sh
cd next13-auth  # Use your project-name instead
rm package-lock.json
yarn
yarn add axios formik next-client-cookies
```

> I prefer using [yarn](https://yarnpkg.com/getting-started/install) over [npm cli](https://docs.npmjs.com/cli/v10/configuring-npm/install). However, you don't have to remove `package.json` if you are using npm commands.

- Start server by running

``` sh
yarn dev
```

- Clean up the code that was generated by nextjs
<br />
- Our final project structure looks like this:

![project structure](https://res.cloudinary.com/dqts6qgre/image/upload/v1695853216/sdev/psf3xqe5neive7ajz1df.png)

> Note: Refer docs for better undertanding of recommended nextjs [project structure](https://nextjs.org/docs/getting-started/project-structure).

<hr />

## Adding basic login page

- Add required type declaration for authentication

``` ts
// /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.

``` tsx
// /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.

<hr />

## Access and modify cookie using custom hook

- Create a custom hook `useCookie.ts` to modify cookie values

``` ts
// /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` and `providers.tsx` inside `app` directory

``` tsx
// /app/cookies.tsx

"use client";

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

export const ClientCookiesProvider: typeof CookiesProvider = (props) => (
  <CookiesProvider {...props} />
);
```

``` tsx
// /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

``` tsx
// /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](https://github.com/moshest/next-client-cookies) for detailed explaination.
<hr />

## Creating `AuthContext.tsx` to persist user

- Create `AuthContext.tsx`

``` 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` in `providers.tsx`

``` tsx
// /app/provider.tsx

<ClientCookiesProvider value={cookies().getAll()}>
  <AuthProvider>{children}</AuthProvider>
</ClientCookiesProvider>
```

<hr />

## 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

``` ts
// /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

``` ts
// /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 };
};
```

<hr />

## Implementation inside components

- Update your login submit handler function with login auth function

``` tsx
// /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:

``` tsx
// /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>
  );
}
```

![Implementation example](https://res.cloudinary.com/dqts6qgre/image/upload/v1695852467/sdev/ppseac5scqnwj8g3x6vb.png)

**Source code**: [nextjs-auth](https://github.com/shinjith-dev/nextjs-auth)

<hr />

### 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!