NEXTJS TODOAPP Tutorial Course

Introduction

Project Requirements

Login requirements.
  • Users should be able to log in using user social media Oauth provider google.
  • Users should be able to log out, ending their session.
  • Implement session management to keep users logged in across different pages.
To Do Requirements
  • Users should be able to create (C in CRUD) new ToDo items. Each ToDo item should have a name, createdData, and updated Date.
  • Users should be able to view or read (R in CRUD) ToDo items.
  • Users should be able to update (U in CRUD) the name.
  • Users should be able to delete (D in CRUD) ToDo items.

App Models

We will have 2 models in our app - User and ToDo.
1model User {
2    id        Int      @id @default(autoincrement())
3    email     String   @unique
4    password  String
5    username  String?  // Optional field
6    createdAt DateTime @default(now())
7    updatedAt DateTime @updatedAt
8    todos     ToDo[]   // Relationship to ToDo model
9  }
10
11  model ToDo {
12    id        Int      @id @default(autoincrement())
13    name      String   
14    status    String   @default("new")
15    createdAt DateTime @default(now())
16    updatedAt DateTime @updatedAt
17    userId    Int      // Foreign key field
18    user      User     @relation(fields: [userId], references: [id])
19  }

Setting Up Project

Install VSCode at VS Code

Install NodeJS at NodeJS

Use below command to setup nextjs project and select "Yes" to all the prompts!
npx create-next-app@latest
Then go to your project and open it in vscode!
Then Launch the app with below command and you should see your app running on http://localhost:3000!
npm run dev

Understanding Folder Structure

Here are main files and folders in nextJS app
  • src - App routers, Layout.tsx, page.tsx and API routers are stored here
  • public - Public assets are stored here
  • package.json - Project dependencies are stored here
  • tsconfig.json - Typescript configuration file
  • next.config.mjs - NextJS configuration file
  • tailwind.config.ts - Tailwind Config file
  • postcss.config.mjs - Postcss Config file
  • README.md - Your project purpose and guide
  • .gitignore - Files that you want git to ignore are stored here
  • .eslintrc.json - ESLint configuration is stored here
We will also create below folders
  • components - We will store reusable components here
  • server - We will store server actions and prisma methods here
  • lib - We will store reusable functions here

Environments and Variable Management

Just create ./.env file or ./.env.local file to store variables. NextJS will load this file automatically. Also add .env file in .gitignore. Please note that git repo is automatically created for you when you created nextjs project! It is recommeded to use .env.local file as it is already added in .gitignore

Writing Backend

PostgressSql

You can get cloud instance of PostgresSql at https://neon.tech/

Prisma

Install Prisma Dependencies
1npm install prisma @prisma/client
2npx prisma init
Add Environment Variables for Database
DATABASE_URL="postgresql://username:password@localhost:5432/mydatabase?schema=public"
Open prisma/schema.prisma and define your data models. For example:
1generator client {
2  provider = "prisma-client-js"
3}
4
5datasource db {
6  provider = "postgresql"
7  url      = env("DATABASE_URL")
8}
9
10
11  model User {
12    id        Int      @id @default(autoincrement())
13    email     String   @unique
14    password  String
15    username  String?  // Optional field
16    createdAt DateTime @default(now())
17    updatedAt DateTime @updatedAt
18    todos     ToDo[]   // Relationship to ToDo model
19  }
20
21  model ToDo {
22    id        Int      @id @default(autoincrement())
23    name      String   
24    status    String   @default("new")
25    createdAt DateTime @default(now())
26    updatedAt DateTime @updatedAt
27    userId    Int      // Foreign key field
28    user      User     @relation(fields: [userId], references: [id])
29  }
Run Prisma migrations to create the database schema
npx prisma migrate dev --name init
Generate the Prisma Client
npx prisma generate
Create Prisma Client Instance
1//src/server/prisma.ts
2
3let prisma;
4
5if (process.env.NODE_ENV === 'production') {
6  prisma = new PrismaClient();
7} else {
8  // In development, use a singleton pattern to prevent multiple instances
9  if (!global.prisma) {
10    global.prisma = new PrismaClient();
11  }
12  prisma = global.prisma;
13}
14
15module.exports = prisma;
Now You can use the Prisma Client
const prisma = require('./prisma');
password field is not needed in user model as we will be using Google provider. So you can ignore this field!

Writing CRUD functions in Prisma

First we will create file to store our CRUD operations. Create file - src/server/dbactions.ts
1// Import PrismaClient singleton
2import { ToDo, User } from "@prisma/client";
3import prisma from "./prisma";
4import { auth } from "@/auth";
5import { redirect } from "next/navigation";
6
7// Function to insert data into the User table
8export async function createUser(email: string = "[email protected]") {
9  const newUser = await prisma.user.create({
10    data: {
11      email: email,
12      password: "hashedpassword123",
13      username: "newuser", // Optional field
14    },
15  });
16  //console.log("User created:", newUser);
17}
18
19// Function to read data from the User table
20export async function getAllUsers() {
21  const users = await prisma.user.findMany();
22  //console.log("All users:", users);
23}
24export async function getToDo(status: string) {
25  const session = await auth();
26
27  if (session?.user?.id === undefined) {
28    throw new Error("Invalid userId");
29  }
30
31  const todos = await prisma.toDo.findMany({
32    where: {
33      userId: parseInt(session?.user?.id), // Filter by user ID
34      ...(status !== "all" && { status: status }), // Filter by status if it's not "all"
35    },
36    orderBy: {
37      updatedAt: "desc", // Sort by the 'updatedAt' field in descending order
38    },
39  });
40
41  return todos;
42
43  // const todos = await prisma.toDo.findMany({
44  //   where: status !== "all" ? { status: status } : {},
45  //   orderBy: {
46  //     updatedAt: "desc", // Sort by the 'updatedBy' field in descending order
47  //   },
48  // });
49  // console.log("To Do list:", todos);
50
51  // return todos;
52}
53
54// Function to save todo
55export async function saveToDo(
56  toDo: Omit<ToDo, "id" | "createdAt" | "updatedAt" | "userId" | "status">
57) {
58  const session = await auth();
59
60  if (typeof session?.user?.id !== "string") {
61    throw new Error("Invalid userId");
62  }
63
64  const newToDo = await prisma.toDo.create({
65    data: {
66      name: toDo.name,
67      userId: parseInt(session?.user?.id), // Link the ToDo to the user
68    },
69  });
70  //console.log("To Do created:", newToDo);
71}
72
73export async function editToDoItem(
74  id: number,
75  toDo: Omit<ToDo, "id" | "createdAt" | "userId" | "updatedAt" | "status">
76) {
77  const updatedToDo = await prisma.toDo.update({
78    where: {
79      id: id,
80    },
81    data: toDo,
82  });
83
84  //console.log("To Do created:", updatedToDo);
85}
86
87export async function deleteToDoItem(id: number) {
88  const deletedTodo = await prisma.toDo.delete({
89    where: {
90      id: id,
91    },
92  });
93}
94
95// Function to find a user by email
96export async function findUserByEmail(email: string) {
97  const user = await prisma.user.findUnique({
98    where: {
99      email: email,
100    },
101  });
102  //console.log("User found:", user);
103}
104
105export async function updateUser(email: string, newData: User) {
106  const updatedUser = await prisma.user.update({
107    where: {
108      email: email,
109    },
110    data: newData,
111  });
112  //console.log("User updated:", updatedUser);
113}
114
115// Function to delete a user by email
116export async function deleteUser(email: string) {
117  const deletedUser = await prisma.user.delete({
118    where: {
119      email: email,
120    },
121  });
122  //console.log("User deleted:", deletedUser);
123}
After adding dbactions.ts file, error will come when importing auth. You can ignore it for now.

Server Actions

Now We can use server actions at src/server/serveractions.ts (using prisma) to interact with database.
1"use server";
2
3import { revalidatePath } from "next/cache";
4import { redirect } from "next/navigation";
5import { deleteToDoItem, editToDoItem, saveToDo } from "@/server/dbactions";
6
7export async function handleFilterChange(status?: string) {
8  console.log("Server action called ", status);
9  //revalidatePath("/" + "?status=" + status);
10  redirect("/" + "?status=" + status);
11}
12
13export async function createToDo(formData: FormData) {
14  const name = formData.get("name");
15
16  if (typeof name !== "string" || name.trim() === "") {
17    throw new Error("Name is required and must be a non-empty string.");
18  }
19
20  const rawFormData = {
21    name: name.trim(),
22  };
23
24  await saveToDo(rawFormData);
25
26  // mutate data
27  // revalidate cache
28  revalidatePath("/");
29}
30
31export async function deleteToDo(id: number) {
32  await deleteToDoItem(id);
33  revalidatePath("/");
34}
35
36export async function editToDo({ id, name }: { id: number; name: string }) {
37  if (typeof name !== "string" || name.trim() === "") {
38    throw new Error("Name is required and must be a non-empty string.");
39  }
40
41  const rawToDoData = {
42    name: name.trim(),
43  };
44
45  await editToDoItem(id, rawToDoData);
46  // mutate data
47  // revalidate cache
48  revalidatePath("/");
49}

Authentication using AuthJS

Create Google Oauth Credentials at https://console.cloud.google.com/apis/credentials . Follow below steps to get Oauth credentials.
  • Click Add credentials and then select Oauth
  • Add Authorized JavaScript origins origin as "http://localhost:3000
  • Add Authorized Redirect Uri as "http://localhost:3000/api/auth/callback/google"
  • Then click Create button. Now you can copy OAuth Client Id and OAuth Client Secret!
Then Set the Env Variables in .env file.
1GOOGLE_CLIENT_ID={GOOGLE_CLIENT_ID}
2GOOGLE_CLIENT_SECRET={GOOGLE_CLIENT_SECRET}
Install Next Auth Dependency.
npm install next-auth@beta
Create AUTH_SECRET environment Variable.
npx auth secret
Add src/auth.ts file
1import NextAuth from "next-auth";
2import Google from "next-auth/providers/google";
3import type { NextAuthConfig, Session } from "next-auth";
4import { createUser } from "./server/dbactions";
5
6export const { handlers, auth } = NextAuth({
7  providers: [
8    Google({
9      clientId: process.env.GOOGLE_CLIENT_ID,
10      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
11    }),
12  ],
13
14  debug: process.env.NODE_ENV === "development",
15
16  callbacks: {
17    async signIn({ user, account, profile, email, credentials }) {
18    
19      // Example: Find or create the user
20      let checkUser = await prisma.user.findUnique({
21        where: { email: user.email },
22      });
23
24      if (!checkUser) {
25        await createUser(user.email!);
26      }
27      return true;
28    },
29
30    async jwt({ user, token, account, profile }) {
31    
32      if (user) {
33        // User object is available only on initial sign-in or sign-up
34        if (!token.id) {
35          // Fetch user from the database and set the custom ID
36
37          const dbUser = await prisma.user.findUnique({
38            where: { email: user.email },
39          });
40
41          if (dbUser) {
42            console.log("setting jwt token");
43            token.id = dbUser.id; // Set custom ID in the token
44          }
45        }
46      }
47
48      return token;
49    },
50
51    async session({ session, token }) {
52     
53      session.user.id = token?.id!?.toString();
54      return session;
55    },
56  },
57} satisfies NextAuthConfig);
Add a Route Handler under /app/api/auth/[...nextauth]/route.ts.
1import { handlers } from "@/auth" // Referring to the auth.ts we just created
2export const { GET, POST } = handlers
auth error in dbactions.ts should go away after adding auth files.

Frontend

Identification of Components and State

When building a Todo app, identifying components and their states is crucial for managing the application's UI and functionality.

Here is the list of components
  • AuthenticationProvider and Appbar: AuthenticationProvider component will wrap our app and provide authentication related features! Appbar component will show profile icon and sign in or sign out button.
  • FilterToDo : Component that will allow user to filter to do items based on if it is completed or not!
  • ToDoList - Component that will show all to do items
  • ToDoForm - Component that allows user to create new to do Item

Authentication in Server Component

Update src/app/page.tsx with below code! This example shows how to check session in server component. We will create FilterToDo, ToDoList and ToDoForm components in upcoming sections.
1import { auth } from "@/auth";
2import { SignInButton, SignInLink } from "@/components/Buttons";
3import FilterToDo from "@/components/FilterToDo";
4import ToDoForm from "@/components/ToDoForm";
5import ToDoList from "@/components/ToDoList";
6import { getToDo } from "@/server/dbactions";
7import Link from "next/link";
8import { redirect } from "next/navigation";
9
10export default async function Home({ searchParams }: { searchParams: any }) {
11  const session = await auth();
12
13  if (!session?.user)
14    return (
15      <>
16        Please &nbsp; <SignInLink /> &nbsp; to view your ToDo items!!
17      </>
18    );
19
20  let status = "all";
21  if (searchParams.status) status = searchParams.status;
22
23  let toDoItems = await getToDo(status);
24
25  return (
26    <main>
27      <FilterToDo />
28      <div className="flex gap-5 flex-col sm:flex-row">
29        <ToDoList toDoItems={toDoItems} />
30        <ToDoForm />
31      </div>
32    </main>
33  );
34}

Authentication in Client Component

In app/layout.tsx, add below code!
1import type { Metadata } from "next";
2import { Inter } from "next/font/google";
3import "@/app/globals.css";
4import { auth } from "@/auth";
5import { AuthProvider } from "@/components/AuthProvider";
6import AppBar from "@/components/AppBar";
7
8const inter = Inter({ subsets: ["latin"] });
9
10export const metadata: Metadata = {
11  title: "Create Next App",
12  description: "Generated by create next app",
13};
14
15export default async function RootLayout({
16  children,
17}: Readonly<{
18  children: React.ReactNode;
19}>) {
20  const session = await auth();
21  return (
22    <html lang="en">
23      <body className="bg-slate-700 text-white px-10 min-h-[80vh]">
24        <AuthProvider session={session}>
25          <AppBar />
26          <div className="py-10 mx-auto max-w-[1000px] flex justify-center">
27            {children}
28          </div>
29        </AuthProvider>
30      </body>
31    </html>
32  );
33}
Create /app/components/AuthProvider.tsx file
1"use client"; //tells Next.js to render this component on the client
2export { SessionProvider as AuthProvider } from "next-auth/react";
Create Buttons Component!
1"use client";
2
3import { useRouter } from "next/navigation";
4import React from "react";
5
6const SignInButton = () => {
7  let router = useRouter();
8  return (
9    <button onClick={() => router.push("/api/auth/signin")}>Sign In</button>
10  );
11};
12
13const SignInLink = () => {
14  let router = useRouter();
15  return (
16    <button
17      className="border-b-2 border-b-white  "
18      onClick={() => router.push("/api/auth/signin")}
19    >
20      Sign In
21    </button>
22  );
23};
24
25export { SignInButton, SignInLink };
Use the session in any client component (e.g. in /src/components/AppBar.tsx) as shown in below example.
1"use client";
2
3import { signIn, signOut, useSession } from "next-auth/react";
4import Link from "next/link";
5import { useRouter } from "next/navigation";
6import React from "react";
7import { SignInButton } from "./Buttons";
8
9const AppBar = () => {
10  const { data: session } = useSession();
11  let router = useRouter();
12
13  return (
14    <div className="flex justify-end gap-3">
15      <Link href="/">Home</Link>
16      <Link href="/dashboard">Dashboard</Link>
17
18      {session?.user ? (
19        <button onClick={() => signOut({ redirect: true, callbackUrl: "/" })}>
20          Sign Out
21        </button>
22      ) : (
23        <SignInButton />
24      )}
25    </div>
26  );
27};
28
29export default AppBar;

FilterToDo Component

In this component (src/components/client/FilterToDo.tsx), we are filtering the to do items using server actions and using push method of router. Recommeded way is to use push method of router as it is client based approach.
1"use client";
2import { handleFilterChange } from "@/server/serveractions";
3import { useRouter } from "next/navigation";
4import React, { useState } from "react";
5
6const FilterToDo = () => {
7  let router = useRouter();
8
9  return (
10    <div className="flex gap-5">
11      {/* <button onClick={() => handleFilterChange("all")}>Show All</button> */}
12
13      <button onClick={() => router.push("/?status=all")}>Show All</button>
14      <button onClick={() => handleFilterChange("new")}>Show New</button>
15      <button onClick={() => handleFilterChange("completed")}>
16        Show Completed
17      </button>
18    </div>
19  );
20};
21
22export default FilterToDo;

ToDo List Items

We will be using some icons from the react icons library so we need to install react-icons package.
npm i react-icons
Create below file src/components/ToDoList.tsx
1"use client";
2import React, { useState } from "react";
3import { ToDo } from "@prisma/client";
4import { deleteToDo, editToDo } from "@/server/serveractions";
5import { RiDeleteBin6Line } from "react-icons/ri";
6import { FaEdit } from "react-icons/fa";
7import { ImSpinner2 } from "react-icons/im"; // Import a loading spinner icon
8
9const ToDoList = ({ toDoItems }: { toDoItems: ToDo[] }) => {
10  const [editingId, setEditingId] = useState<number | null>(null);
11  const [editValue, setEditValue] = useState<string>("");
12  const [loadingId, setLoadingId] = useState<number | null>(null); // Track loading state for edit
13  const [deletingId, setDeletingId] = useState<number | null>(null); // Track loading state for delete
14
15  const handleEditClick = (id: number, name: string) => {
16    setEditingId(id);
17    setEditValue(name);
18  };
19
20  const handleSaveClick = async (id: number) => {
21    setLoadingId(id); // Set loading state for the current ID
22    try {
23      await editToDo({
24        id,
25        name: editValue,
26      });
27    } finally {
28      setLoadingId(null); // Clear loading state
29      setEditingId(null); // Exit editing mode
30    }
31  };
32
33  const handleDeleteClick = async (id: number) => {
34    setDeletingId(id); // Set loading state for delete
35    try {
36      await deleteToDo(id);
37    } finally {
38      setDeletingId(null); // Clear loading state
39    }
40  };
41
42  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
43    setEditValue(e.target.value);
44  };
45
46  return (
47    <div>
48      <h1 className="m-5">ToDo List</h1>
49      <ul className="leading-10 list-disc list-inside bg-gray-100 px-20 rounded-lg shadow-md">
50        {toDoItems.map((todo) => (
51          <li
52            key={todo.id}
53            className="text-gray-800 py-2 border-b border-gray-300 last:border-b-0 flex justify-between items-center"
54          >
55            {editingId === todo.id ? (
56              <input
57                type="text"
58                value={editValue}
59                onChange={handleChange}
60                className="p-2 border border-gray-300 rounded"
61              />
62            ) : (
63              <span>{todo.name}</span>
64            )}
65            <div className="flex space-x-2 items-center">
66              {editingId === todo.id ? (
67                <>
68                  <button
69                    className="text-green-500 hover:text-green-700 p-2 flex items-center justify-center relative"
70                    onClick={() => handleSaveClick(todo.id)}
71                  >
72                    {loadingId === todo.id ? (
73                      <ImSpinner2 className="animate-spin w-5 h-5" />
74                    ) : (
75                      "Save"
76                    )}
77                  </button>
78                </>
79              ) : (
80                <button
81                  className="text-blue-500 hover:text-blue-700 p-2 flex items-center justify-center"
82                  onClick={() => handleEditClick(todo.id, todo.name)}
83                >
84                  <FaEdit className="w-5 h-5" />
85                </button>
86              )}
87              <button
88                className="text-red-500 hover:text-red-700 p-2 flex items-center justify-center relative"
89                onClick={() => handleDeleteClick(todo.id)}
90              >
91                {deletingId === todo.id ? (
92                  <ImSpinner2 className="animate-spin w-5 h-5" />
93                ) : (
94                  <RiDeleteBin6Line className="w-5 h-5" />
95                )}
96              </button>
97            </div>
98          </li>
99        ))}
100      </ul>
101    </div>
102  );
103};
104
105export default ToDoList;

ToDo Form

Create below file src/components/ToDoForm.tsx
1import { createToDo } from "@/server/serveractions";
2import React from "react";
3
4const ToDoForm = () => {
5  return (
6    <div>
7      <h1 className="m-5">ToDo Form</h1>
8      <form
9        action={createToDo}
10        className="max-w-sm mx-auto p-4 bg-white shadow-md rounded-lg"
11      >
12        <div className="mb-4">
13          <label className="block text-gray-700 font-bold mb-2" htmlFor="name">
14            ToDo Name:
15          </label>
16          <input
17            className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 text-black"
18            type="text"
19            name="name"
20            id="name"
21            required
22          />
23        </div>
24        <button
25          type="submit"
26          className="w-full bg-blue-500 text-white font-bold py-2 px-4 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
27        >
28          Add ToDo
29        </button>
30      </form>
31    </div>
32  );
33};
34
35export default ToDoForm;
Create dashboard page.
1import { auth } from "@/auth";
2import { SignInLink } from "@/components/Buttons";
3
4export default async function Dashboard() {
5  const session = await auth();
6
7  if (!session?.user)
8    return (
9      <>
10        Please &nbsp; <SignInLink /> &nbsp; to view your Dashboard!
11      </>
12    );
13
14  return (
15    <div className="flex justify-center place-content-center shadow-xl border-1 rounded-md bg-slate-600 flex-col p-10">
16      <div>
17        <span>Hello</span>
18        <span className="px-2 text-2xl">{session?.user?.name},</span>
19      </div>
20      <div className="my-5">
21        Email : <span className="px-2">{session?.user?.email}</span>
22      </div>
23    </div>
24  );
25}

Going Live

Deploy to Vercel

Install dependency.
npm install -g vercel
Create build
npm run build
Deploy using Vercel Command line
1vercel login
2cd /path/to/your/nextjs-app
3vercel
Ensure that environment variables needed for your project are set in vercel project settings! By linking your Git repository, Vercel automatically redeploys your app whenever you push changes to your GitHub, GitLab, or Bitbucket repository.

Custom Domain

If you have a custom domain, you can add it to your Vercel project under the "Domains" tab.

Checking Logs and Metrics

You can look at logs of your in app in vercel dashboard!

Securing App

Sanitizing Forms

Sanitizing form inputs in NextJS is crucial for protecting your application from security vulnerabilities such as Cross-Site Scripting (XSS) and ensuring that the data submitted by users is clean and formatted correctly.

Install dompurify and validator packages!
1npm install dompurify
2npm install validator
You can use dompurify and validator to sanitize the form data in NextJS as shown in below example!
1'use client'
2import React, { useState } from 'react';
3import DOMPurify from 'dompurify';
4import validator from 'validator';
5
6const Form = () => {
7  const [input, setInput] = useState('');
8  const [email, setEmail] = useState('');
9  const [error, setError] = useState('');
10
11  const handleInputChange = (e) => {
12    const rawInput = e.target.value;
13
14    // Trim and remove special characters
15    const sanitizedInput = rawInput.trim().replace(/[^ws]/gi, '');
16    setInput(sanitizedInput);
17  };
18
19  const handleEmailChange = (e) => {
20    const rawEmail = e.target.value;
21
22    // Escape HTML and validate email
23    const sanitizedEmail = DOMPurify.sanitize(rawEmail);
24    if (validator.isEmail(sanitizedEmail)) {
25      setEmail(sanitizedEmail);
26      setError('');
27    } else {
28      setError('Invalid email address');
29    }
30  };
31
32  const handleSubmit = (e) => {
33    e.preventDefault();
34    // Proceed with the sanitized and validated input
35    console.log('Sanitized Input:', input);
36    console.log('Sanitized and Validated Email:', email);
37  };
38
39  return (
40    <form onSubmit={handleSubmit}>
41      <div>
42        <label>Text Input:</label>
43        <input
44          type="text"
45          value={input}
46          onChange={handleInputChange}
47        />
48      </div>
49      <div>
50        <label>Email:</label>
51        <input
52          type="email"
53          value={email}
54          onChange={handleEmailChange}
55        />
56        {error && <p style={{ color: 'red' }}>{error}</p>}
57      </div>
58      <button type="submit">Submit</button>
59    </form>
60  );
61};
62
63export default Form;

Stopping Bots using Captcha

Turnstile is a CAPTCHA alternative provided by Cloudflare, designed to verify that a user is human without requiring them to complete traditional CAPTCHA challenges. Integrating Turnstile into a React form involves several steps, including setting up Turnstile on the server-side and then integrating it into your React application.
npm install @marsidev/react-turnstile
Here is a sample code to embed this widget in the react forms!
1'use client'
2
3          import React, { useState } from 'react';
4import Turnstile from '@marsidev/react-turnstile';
5
6const MyForm = () => {
7  const [captchaToken, setCaptchaToken] = useState('');
8
9  const handleSubmit = async (e) => {
10    e.preventDefault();
11
12    // Submit form data along with the Turnstile token
13    const formData = {
14      // Your form data
15      captchaToken,
16    };
17
18    // Example: POST formData to your server
19    const response = await fetch('/api/submit-form', {
20      method: 'POST',
21      headers: {
22        'Content-Type': 'application/json',
23      },
24      body: JSON.stringify(formData),
25    });
26
27    const result = await response.json();
28    console.log(result);
29  };
30
31  return (
32    <form onSubmit={handleSubmit}>
33      <div>
34        <label>Your Name:</label>
35        <input type="text" name="name" required />
36      </div>
37      <div>
38        <label>Your Email:</label>
39        <input type="email" name="email" required />
40      </div>
41
42      {/* Turnstile CAPTCHA */}
43      <Turnstile
44        sitekey="your-site-key-here"
45        onSuccess={(token) => setCaptchaToken(token)}
46        onError={() => setCaptchaToken('')}
47      />
48
49      <button type="submit" disabled={!captchaToken}>Submit</button>
50    </form>
51  );
52};
53
54export default MyForm;

Rate Limiting

Since we are not using any API in nextjs 14, we do not need to do rate limiting for them!

WAF - Web Application Firewall

A Web Application Firewall (WAF) works by filtering and monitoring HTTP/HTTPS traffic between a web application and the internet to protect against various web-based threats. WAFs inspect incoming HTTP/HTTPS requests to identify and block malicious traffic. They check requests against predefined rules or patterns to detect potentially harmful content. WAFs use signatures and patterns to identify known attack vectors, such as SQL injection strings or XSS payloads.

Some examples
  • Block requests containing SQL keywords such as SELECT, UNION, or DROP in URL parameters or request bodies.
  • Block requests containing script tags (script) or JavaScript events (onerror, onclick) in user input.
  • Block uploads of executable files (.exe, .php) or files larger than a specified size.
  • Block requests using methods like TRACE or DELETE if not required by the application.
  • Block known malicious IP addresses or allow only certain trusted IP ranges.
  • Block requests with suspicious patterns, such as rapid request rates or known bot signatures.
  • Allow a maximum of 100 requests per IP address per minute.

Scaling App

Using middleware

  • Rate Limiting is usually implemented in middleware!
  • Cache responses to reduce the load on your application and database by serving cached content for frequently accessed requests.
  • Reduce the size of the response data sent to clients, improving load times and reducing bandwidth usage.
  • Manage user authentication and authorization, ensuring that only authorized users can access certain resources.
  • Log requests and responses for monitoring and debugging purposes.
  • Protect your application from common security threats, such as cross-site scripting (XSS) and clickjacking, which can impact performance and reliability. CSP headers can be injected in server components.

Horizontal Scaling

  • Containerization and K8s
  • Load Balancing
  • Auto Scaling

Vertical Scaling

  • Upgrade Instance Size
  • Database Optimization

Caching

  • In-Memory Caching - Redis/Memcached
  • HTTP Caching - Set appropriate cache headers (e.g., Cache-Control, Expires) to enable client-side caching of static resources
  • Content Delivery Network (CDN)

Asynchronous Processing

  • Background jobs - Use job queues like Bull or Kue to handle background tasks asynchronously. Implement worker processes or microservices to handle long-running tasks separately from your main application.
  • Use message brokers (RabbitMQ/Kafka) to manage communication between different components of your application and handle distributed tasks.

Monitoring and logging

  • Use tools like Prometheus, Grafana, or New Relic to monitor application performance, resource usage, and response times.
  • Use centralized logging systems like ELK Stack (Elasticsearch, Logstash, Kibana) or Loki to aggregate and analyze logs from multiple instances.