Home

Next.js

Installation

For installing in the current directory ”./”

npx create-next-app@latest ./

Server- vs Client Components

  • On default components run on the server

To run a component on the client, type on the top of the page:

"use client"

Client rendering needs to be used for:

  • interactivity and event listeners

  • useState or useEffect

  • using browser only APIs

Project Structure

app/ or pages/
├── users/
│   └── route.js              => /users (API route)
├── blog/
│   └── page.tsx              => /blog
├── ...                       => other routes & components
lib/
├── connectDb.js              => reusable DB utilities
models/
├── User.js                   => mongoose/ORM models
public/
components/
styles/                       => CSS files
types/                        => TypeScript definitions
.env
next.config.js
package.json

Routing

  • all Routes need to be inside the app folder

  • files representing a route should be named: page.js / page.tsx

  • every folder is a path segment in the URL

app/
├── page.tsx            => /
├── about/
│   └── page.tsx        => /about
├── blog/
│   ├── page.tsx        => /blog
│   └── [id]/
│       └── page.tsx    => /blog/random-name
// Dynamic routes with brackets (id or slug):
// params are automatically received as props
export default async function Blog({params}: {params:{id:string}}){
  const {id} = await params; // async since nextjs v5
  return <h1>Product</h1>
}

Logical Routes Grouping

app/
├── page.tsx            => /
├── (auth)/
    ├── register/
    │   └── page.tsx    => /register
    └── login/
        └── page.tsx    => /login
        
# No need to type "auth" in the url

Layouts

Share UI between multiple pages, e.g. navigation, footer

layout.tsx

 ...
 return (
    <html lang="en">
      <body>
        <Nav></Nav> // will be visible on all pages
        {children}
        <Footer></Footer>  // will be visible on all pages
      </body>
    </html>
  );
  
  // Place content above or beneath the children props

Nested Layouts

Place a layout.tsx in a folder which route would need this specific layout

app/
├── page.tsx            
├── products/       
│   └── [id]/
│       ├── layout.tsx    
│       └── page.tsx


# products/[id]/layout.tsx
export default function ProductLayout({children}: {children: React.ReactNode}){
  return (
    <div>
      {children} // renders the component from products/[id]/page.tsx
      <h2>Product Header</h2>  // will show on all products pages
    <div>
  )
}
"use client"

import Link from "next/link";
import {usePathname} from "next/navigation";

const pathname = usePathname();

<Link href="/" className={pathname ? "light" : "dark"}>Home</Link>

Programatic Navigation

"use client"

import {useRouter} from "next/navigation";
const router = useRouter();

<button onClick={()=>router.push("/")}>Back Home</button>

Route Handlers (API Endpoints)

Create RESTful endpoints

app/ 
├── users/       
│      ├── route.ts # name is mandatory

# Request: http://localhost:3000/users

users/route.ts

import User from "../models/User.ts";

// GET handler
export async function GET() {
  try {
    const users = User.find();
    return Response.json(users, { status: 200 });
  } catch (error) {
    return Response.json(
      { message: "Error fetching users" },
      { status: 500 }
    );
  }
}

// POST handler
export async function POST(request: Request) {
    try {
      const body = await request.json();
      const newUser = User.create(body);
      return Response.json(newUser);
    } catch (error){
        return Response.json(
            { message: "Error fetching users" },
            { status: 500 }
        );
    }
}

Dynamic Route Handler

app/         
├── users/       
│   └── [id]/   
│       └── route.ts


# users/route.ts
import User from "../models/User.ts";

export async function GET(request:Request, {params}:{params: {id: string}}) {
  try {
    const {id} = await params;
    const user = User.findById(id);
    return Response.json(user);
  } catch (error) {
    return Response.json(
      { message: "Error fetching user" },
      { status: 500 }
    );
  }
}

Params

// URL Request: http://localhost:3000/12345

export async function GET(request:Request, {params}:{params: {id: string}}) {}

const {id} = await params; // id = 12345

Query

// URL Request: http://localhost:3000?search=shirt

const { searchParams } = new URL(request.url);
const search = searchParams.get('search'); // search = "shirt"

Mongoose Model

A check if the model already exists is necessary and to prevent creating multiple instances of a model

import { Schema, model, models } from 'mongoose';

export default models.Post || model("User", userSchema);

Database Connection

Since next.js does not have an entry point like a server.js, the connection needs to be established on an api call

or the experimental instrumentation feature can be used:

next.config.ts

module.exports = {
    experimental: {
        instrumentationHook: true,
    },
}

instrumentation.js (at root level of the project)

// import connectDb to connect with MongoDB

export async function register() { // function name is mandatory
    await connectDb()
}

Without instrumentation

libs/connectDb.ts

let isConnected = false;

export default function connectDB() {
  if (isConnected) return;

  // code to connect to the database
  
  isConnected = true;
 }

Fetching (Server Side)

Next.js handles the fetch in page.tsx automatically with the content of error.tsx and loading.tsx

Example structure:

app/         
├── users-server/       # random name
│   ├── error.tsx       # naming is mandatory
│   ├── loading.tsx     # naming is mandatory
│   └── page.tsx

# user-server/loading.tsx
export default function Loading(){
  return <div>Loading...</div>
}

user-server/error.tsx

"use client"

import {useEffect} from "react";

export default function Error({error}:{error: Error}){
  useEffect(()=>{
    console.log(error);
  },[error])

  return <div>Error fetching users data!</div>
}

user-server/page.tsx

export default async function UsersServer(){

  type User = {
    name: string,
    email: string
  };
  
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = await response.json();

  return (
    <div>{user.map(user:User)=>(
      <p>user.name</p>
    )}
    </div>
  )
}

Server Actions

Asynchronous functions executed on the server

For form submissions, updating database, operations which require server-side execution

File structure:

app/         
├── mock-users/       # random name
│   └── page.tsx


# mock-users/page.tsx
import {revalidatePath} from "next/cache"; // to avoid manual page reload

export default async function MockUsers(){
  
  const response = await fetch("https://2032aa902f238eß472d20.mockapi.io/users");
  const users = await response.json();
  
  // server action
  async function addUser(formData: FormData){
    "use server"
    const name = formData.get("name");
    
    const response = await fetch("https://2032aa902f238eß472d20.mockapi.io/users", 
      {
        method:"POST",
        headers: {
          "Content-Type": "applicaton/json",
          // Auth would go here
          // Authorization: "Bearer YOUR-PRIVATE-KEY"
          },
        body: JSON.stringify({name})
      });
      const newUser = response.json();
      revalidatePath("mock-users"); // updates data for this path (no reload needed)
      console.log(newUser);
  };

  return (
    <div>
      <form action={addUser}> // name of server action function
        <input type="text" id="name" name="name"> 
        <button>Add User</button>
      </form>
    </div>
  )
}