thoughtSpace
TwitterGithubRSS Feed

Note Space

Hints, cheat sheets and notes on code.

Home

NextJS Crash Course

Posted on April 7, 2022
nextjs-topic

In Next.js, we can pre-render a page during build time. We generate all the HTML code and data in advance. And the data is later cached by the server.

This approach works well for the static paths but, it fails when we have to pre-render pages for dynamic paths. And it makes sense. Let's say there is a blog and, there are multiple articles under it. And it next.js we defined dynamic paths like [blogId].js. As we already know, this path is valid for blog id 1, 2, 3, 4, and so on. There is no way Next.js knows how many pages it has to render.

To accomplish this, getStaticPath() is another function we use in this article.

Benefits of using Next

  • No need to write separate backends for projects one wants to create. One can build an entire project with Next.js.
  • One never has to worry about bundler, compiler, or frontend infrastructure. Once gets to focus on making great products through React components. And one is able to use the latest React features.
  • One is able to update to the latest versions of Next.js and things continue to improve. Performance gets faster and new features get added. The iteration velocity is high. If there are changes, codemods and upgrade guides are provided.
  • Next.js provides a bunch of components that help one keep his/her site fast. Images, fonts, scripts, and now even properly loading third-parties.

Security Approaches

  • HTTP APIs (recommended for existing large projects / orgs) Call custom API endpoints such as REST or GraphQL using fetch() from Server Components just like if it was executing on the client. Passing along any cookies.

  • Data Access Layer (recommended for new projects):

    • The principle is that a Server Component function body should only see data that the current user issuing the request is authorized to have access to.
    • By using Data Transfer Objects as illustrated below, this creates a layering where security audits can focus primarily on the Data Access Layer while the UI can rapidly iterate. Smaller surface area and less code to cover makes it easier to catch security issues.
    • Secret keys can be stored in environment variables but only the data access layer should access process.env in this approach. ```typescript // data/auth.tsx import { cache } from 'react'; import { cookies } from 'next/headers';

    // Cached helper methods makes it easy to get the same value in many places // without manually passing it around. This discourages passing it from Server // Component to Server Component which minimizes risk of passing it to a Client // Component. export const getCurrentUser = cache(async () => {

    const token = cookies().get('AUTH_TOKEN');
    const decodedToken = await decryptAndValidate(token);
    // Don't include secret tokens or private information as public fields.
    // Use classes to avoid accidentally passing the whole object to the client.
    return new User(decodedToken.id);
    

    });

    // data/user-dto.tsx (Data Transfer Object) import 'server-only'; import { getCurrentUser } from './auth';

    function canSeeUsername(viewer: User) {

    // Public info for now, but can change
    return true;
    

    }

    function canSeePhoneNumber(viewer: User, team: string) {

    // Privacy rules
    return viewer.isAdmin || team === viewer.team;
    

    }

    export async function getProfileDTO(slug: string) {

    // Don't pass values, read back cached values, also solves context and easier to make it lazy
    
    // use a database API that supports safe templating of queries
    const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`;
    const userData = rows[0];
    
    const currentUser = await getCurrentUser();
    
    // only return the data relevant for this query and not everything
    // <https://www.w3.org/2001/tag/doc/APIMinimization>
    return {
      username: canSeeUsername(currentUser) ? userData.username : null,
      phonenumber: canSeePhoneNumber(currentUser, userData.team)
        ? userData.phonenumber
        : null,
    };
    

    }

    import {getProfile} from '../../data/user' export async function Page({ params: { slug } }) {

    // This page can now safely pass around this profile knowing
    // that it shouldn't contain anything sensitive.
    const profile = await getProfile(slug);
    ...
    

    } ```

  • Component Level Data Access (recommended for prototyping and learning)

    • Put your database queries directly in your Server Components
    • A Client Component should not accept more data than the minimal data it needs to perform its job.
    • This approach is only appropriate for rapid iteration and prototyping. E.g. for a small product with a small team where everyone is aware of the risks and how to watch for them.
    • Always use parameterized queries, or a db library that does it for you, to avoid SQL injection attacks.
    • Pass down a limited object from Server Page to Client child component with just the fields necessary for rendering the profile.
  • It's better to avoid data getting into the Server Components in the first place - using a Data Access Layer

  • By default, environment variables are only available on the Server. By convention, Next.js also exposes any environment variable prefixed with NEXT_PUBLIC_ to the client

  • Always re-read access control and cookies() whenever reading data. Don't pass it as props or params.

  • Rendering a Server Component should never perform side-effects like mutations. Additionally, in Next.js there's no way to set cookies or trigger revalidation of caches during rendering. This also discourages the use of renders for mutations. E.g. searchParams should not be used to perform side-effects like saving changes or logging out. Server Actions should be used for this instead

  • This means that the Next.js model never uses GET requests for side-effects when used as intended. This helps avoid a large source of CSRF issues. The idiomatic way to perform writes or mutations in Next.js App Router is using Server Actions. ```typescript // actions.ts

'use server';

export function logout() { cookies().delete('AUTH_TOKEN'); }

export async function deletePost(id: number) { if (typeof id !== 'number') { // The TypeScript annotations are not enforced so // we might need to check that the id is what we // think it is. throw new Error(); } const user = await getCurrentUser(); if (!canDeletePost(user, id)) { throw new Error(); } ... }

- The "use server" annotation exposes an end point that makes all exported functions invokable by the client. The identifiers is currently a hash of the source code location. As long as a user gets the handle to the id of an action, it can invoke it with any arguments.

- These functions should always start by validating that the current user is allowed to invoke this action. 
- Functions should also validate the integrity of each argument. This can be done manually or with a tool like zod

### CSRF
- All Server Actions can be invoked by plain <form>, which could open them up to CSRF attacks. 
- Behind the scenes, Server Actions are always implemented using POST and only this HTTP method is allowed to invoke them. This alone prevents most CSRF vulnerabilities in modern browsers, particularly due to **Same-Site cookies** being the default.

- As an additional protection Server Actions in Next.js 14 also compares the Origin header to the Host header (or X-Forwarded-Host). 
- If they don't match, the Action will be rejected. In other words, Server Actions can only be invoked on the same host as the page that hosts it. 
- In production mode, React doesn't emit errors or rejected promises to the client. Instead a hash is sent representing the error.
- In development mode, server errors are still sent in plain text to the client to help with debugging
- It's important to always run in Next.js in production mode for production workloads. Development mode does not optimize for security and performance.
- Middleware can be used to limit access to certain pages. Usually it's best to do this with an allow list rather than a deny list.

- When errors are thrown on the Server they are eventually rethrown in Client code to be handled in the UI

## Auditing A Next Application
- Data Access Layer. Is there an established practice for an isolated Data Access Layer? Verify that database packages and environment variables are not imported outside the Data Access Layer.
- "use client" files. Are the Component props expecting private data? Are the type signatures overly broad?
- "use server" files. Are the Action arguments validated in the action or inside the Data Access Layer? Is the user re-authorized inside the action?
- /[param]/. Folders with brackets are user input. Are params validated?
- middleware.tsx and route.tsx have a lot of power. Spend extra time auditing these using traditional techniques. Perform Penetration Testing or Vulnerability Scanning regularly or in alignment with your team's software development lifecycle.

## Snippets and meaning

```typescript
import 'server-only'; //Code that should only ever execute on the server, helps avoid data leaks from server to client

// In Next.js 14 release, one can also try out the experimental React Taint APIs by enable the taint flag in next.config.js.
// next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
};

// mark an object that should not be allowed to be passed to the client as is.
import { experimental_taintObjectReference } from 'react';
 
export async function getUserData(id) {
  const data = ...;
  experimental_taintObjectReference(
    'Do not pass user data to the client',
    data
  );
  return data;
}

// page.tsx
import { getUserData } from './data';
 
export async function Page({ searchParams }) {
  const userData = getUserData(searchParams.id);
  return <ClientComponent user={userData} />; // error
}

Debugging Next in vs code

https://nextjs.org/docs/pages/building-your-application/configuring/debugging#debugging-with-vs-code

Tips and reads

  • For initial load Next.js will run both the Server Components and the Client Components on the server to produce HTML.

  • Route Handlers are only available inside the app directory. They are the equivalent of API Routes inside the pages directory meaning you do not need to use API Routes and Route Handlers together

  • Route Handlers are defined in a route.js|ts file inside the app directory(inside an api folder)

  • Route Handlers are cached by default when using the GET method with the Response object.

  • Opting out of caching

    • Using the Request object with the GET method.
    • Using any of the other HTTP methods.
    • Using Dynamic Functions like cookies and headers.
    • The Segment Config Options manually specifies dynamic mode.
// app/items/route.ts

export async function GET() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY,
    },
  })
  const data = await res.json()
 
  return Response.json({ data })
}

// You can revalidate cached data using the next.revalidate option:

// app/items/route.ts


export async function GET() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    next: { revalidate: 60 }, // Revalidate every 60 seconds
  })
  const data = await res.json()
 
  return Response.json(data)
}

// Cookies
import { cookies } from 'next/headers'
 
export async function GET(request: Request) {
  const cookieStore = cookies()
  const token = cookieStore.get('token')
 
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: { 'Set-Cookie': `token=${token.value}` },
  })
}

// Cookies Alternative
import { type NextRequest } from 'next/server'
 
export async function GET(request: NextRequest) {
  const token = request.cookies.get('token')
}
// CORS
export async function GET(request: Request) {
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

Useful frontend packages

  • @tryghost/content-api - Used to interact with ghost cms
  • gsap - robust JavaScript animation library
  • nodemailer -Send emails from Node.js
  • nodemailer-mailgun-transport - node module to send emails within any of the nodejs apps. This is the transport plugin that goes with nodemailer to send email using Mailgun's awesomenes
  • nprogress - A nanoscopic progress bar. Featuring realistic trickle animations to convince your users that something is happening
  • react-hook-form - Easy to use form valdiation
  • react-map-gl - react friendly API wrapper around MapboxGL JS
  • use-is-in-viewport - react hook to use the IntersectionObserver declaratively in a React app for the purposes of finding out if an element is in a given viewport.
  • @radix.ui - component library optimized for fast development, easy maintenance, and accessibility.
  • nookies - A collection of cookie helpers for Next.js
  • tailwind-merge - Utility function to efficiently merge Tailwind CSS classes in JS without style conflicts
  • lucide-react - Implementation of the lucide icon library for react applications.
  • clsx - Utility for constructing className strings conditionally
  • Class Variance Authority - CSS-in-TS libraries
  • tailwindcss-animate - Tailwind CSS plugin for creating beautiful animations.
  • Zustand - A small, fast and scalable bearbones state-management solution using simplified flux principles
  • zod - TypeScript-first schema validation with static type inference
  • yup - A schema builder for runtime value parsing and validation
  • wait-on - cross-platform command line utility which will wait for files, ports, sockets, and http(s) resources to become available (or not available using reverse mode).
  • useDebounce - value and callback debouncing(delay) i.e. ensure that time-consuming tasks do not fire so often, that it stalls the performance of the web page
  • sweetalert2 - A beautiful, responsive, customizable, accessible (WAI-ARIA) replacement
  • for JavaScript's popup boxes
  • sharp - convert large images in common formats to smaller, web-friendly JPEG, PNG, WebP, GIF and AVIF images of varying dimensions.
  • react-query - Powerful asynchronous state management
  • react-table - Headless UI for building powerful tables & datagrids
  • libphonenumber-js - phone number formatting and parsing library
  • jspdf - A library to generate PDFs in JavaScript.
  • image-to-base64 - Generate a base64 code from an image through a URL or a path.
  • html2canvas - take "screenshots" of webpages or parts of it, directly on the users browser.
  • file-saver - the solution to saving files on the client-side, and is perfect for web apps that generates files on the client
  • cors - CORS is a node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options.
  • antd - An enterprise-class UI design language and React UI library.
  • @hookform/resolvers - Performant, flexible and extensible forms with easy to use validation.
  • @apollo/client - Apollo Client is a fully-featured caching GraphQL client with integrations for React, Angular, and more.
  • next-themes - An abstraction for themes in your Next.js app.

Be

Note Space © 2022 — Published with Nextjs

HomeTopicsLinksDefinitionsCommandsSnippetsMy works