Techtales.

NextJS Best Practices for Beginners
TD
The Don✨Author
Aug 12, 2025
11 min read

NextJS Best Practices for Beginners

00

Next.js has become the go-to React framework for building modern web applications. It provides server-side rendering (SSR), static site generation (SSG), streaming, and built-in caching out of the box. But with great power comes great responsibility: how you use these features determines whether your app is blazing fast or frustratingly slow.

In this guide, we’ll walk through Next.js best practices every beginner should follow, with code examples to make it easier to apply in your own projects.

Data Fetching Strategy: Server vs Client

NextJS suggests that you should always fetch data on the server if possible using server actions. This means that you can make the root page a server component, fetch data and then pass the data to children that are marked as "use client". Fetching data on the server allows you to:

  • Have direct access to backend data resources (e.g. databases).
  • Keep your application more secure by preventing sensitive information, such as access tokens and API keys, from being exposed to the client.
  • Fetch data and render in the same environment. This reduces both the back-and-forth communication between client and server, as well as the work on the main thread on the client.
  • Perform multiple data fetches with single round-trip instead of multiple individual requests on the client.
  • Reduce client-server waterfalls.
  • Depending on your region, data fetching can also happen closer to your data source, reducing latency and improving performance.

✅ Good: Server-side data fetching

Layout Data Fetching: The Dynamic Rendering Trap

One of the worst performance mistakes that you can make in NextJS is to fetch data on the root layout and pass it to the children. One common scenario is when you want to fetch the user session and render it in the navbar since doing so would reduce the flickering and ensure session is available each time. While this is enticing, doing so will convert all the routes in the layout to dynamic rendering regardless of whether they are server or client routes.

❌ Don't do this:

The best approach is to create a navbar component and fetch the necessary data there. NextJS notes that if you need to use the same data (e.g. current user) in multiple components in a tree, you do not have to fetch data globally, nor forward props between components. Instead, you can use fetch or React cache in the component that needs the data without worrying about the performance implications of making multiple requests for the same data. This is possible because fetch requests are automatically memoized.

✅ Better approach: Use client components for dynamic layout data

In most cases, UserNav will not be a server component since it requires some interactivity. In that case, I would recommend using a Session Provider to wrap the children in the layout, or using React-Query to fetch the user session data in the navbar.

Why This Matters:

  • Static pages stay static: Your blog posts, marketing pages, and documentation can still be pre-rendered
  • Better performance: Static pages load instantly from CDN
  • Lower server costs: Fewer dynamic requests to handle

Okay

Caching: Your Performance Superpower

This is a very sensitive topic and a cause of pitfall in most NextJS apps. I would recommend you read the documentation: Guides: Caching | Next.js. Due to a lot of complains that NextJS was going full "god-mode" on caching, the Vercel has now allowed users to opt into caching in their components.

Understanding Next.js Caching Layers

  1. Request Memoization: Automatic deduplication of identical requests
  2. Data Cache: Persistent cache across requests and deployments
  3. Full Route Cache: Static rendering cache
  4. Router Cache: Client-side navigation cache This is some basic, sample markdown.

Caching can also be a great source of headache if you fail to invalidate existing cache after mutations. When using server actions, NextJS provides unstable_cache function that allows you to pass custom tags to invalidate the cache.

Best Practice:

  • Use revalidate to control when data should be refreshed.

  • Use revalidatePath or revalidateTag when you mutate data (like after form submissions or mutations).

Streaming and Loading States

Streaming and Suspense are React features that allow you to progressively render and incrementally stream rendered units of the UI to the client.

With Server Components and nested layouts, you're able to instantly render parts of the page that do not specifically require data, and show a loading state for parts of the page that are fetching data. This means the user does not have to wait for the entire page to load before they can start interacting with it.
Server Rendering with Streaming

Best Practice:

  • Wrap slow components in <Suspense> so the page can load progressively.

  • Provide a fallback UI (loading state).

Another alternative is to create a loading.tsx file in the path folder.

Data Preloading and Parrarel Fetching

When fetching data inside React components, you need to be aware of two data fetching patterns: Parallel and Sequential.

Sequential and Parallel Data Fetching

With sequential data fetching, requests in a route are dependent on each other and therefore create waterfalls. There may be cases where you want this pattern because one fetch depends on the result of the other, or you want a condition to be satisfied before the next fetch to save resources.

However, this behavior can also be unintentional and lead to longer loading times. With parallel data fetching, requests in a route are eagerly initiated and will load data at the same time. This reduces client-server waterfalls and the total time it takes to load data.

Sequential Data Fetching

If you have nested components, and each component fetches its own data, then data fetching will happen sequentially if those data requests are different (this doesn't apply to requests for the same data as they are automatically memoized). For example, the Playlists component will only start fetching data once the Artist component has finished fetching data because Playlists depends on the artistID prop:

In cases like this, you can use loading.js (for route segments) or React <Suspense> (for nested components) to show an instant loading state while React streams in the result.

This will prevent the whole route from being blocked by data fetching, and the user will be able to interact with the parts of the page that are not blocked.

Parallel Data Fetching

To fetch data in parallel, you can eagerly initiate requests by defining them outside the components that use the data, then calling them from inside the component. This saves time by initiating both requests in parallel, however, the user won't see the rendered result until both promises are resolved.

In the example below, the getArtist and getArtistAlbums functions are defined outside the Page component, then called inside the component, and we wait for both promises to resolve:

Preloading Data

Another way to prevent waterfalls is to use the preload pattern. You can optionally create a preload function to further optimize parallel data fetching. With this approach, you don't have to pass promises down as props. The preload function can also have any name as it's a pattern, not an API.

Image Optimization

Best Practices Images are often stressful since they might require alot of resources. Luckily, NextJS image component allows us to optimize images and only serve images in the required sizes. You can optimize this further by adding a blur url and a fallback. For example, I use this BlogImage component to ensure my blogs will always have a cover image:

Route Organization and File Structure

Another of the most common challenges that you can face is separating layouts for different pages. For example you might need to show Navbar and Footers only in the main pages and not the Auth routes. This means for the Auth pages, you must have a dedicated AuthLayout. However, if you fail to separate the folders, there might be some mix ups.

Server Actions vs API Routes: The Modern Next.js Approach

One of the selling point for NextJS is that it is a fullstack framework that allows you to create a server in your api routes. Any routes in the api folder with route.ts are automatically server routes. However, with server actions, you might not need to clutter your application with unnecessary api routes.

Server Actions represent one of the most significant improvements in Next.js App Router, fundamentally changing how we handle server-side logic. If you're still reaching for API routes for every form submission or data mutation, you're missing out on a more elegant, performant, and type-safe approach.

What Are Server Actions?

Server Actions are asynchronous functions that run on the server and can be called directly from React components. They're marked with the 'use server' directive and provide a seamless way to handle server-side operations without the complexity of API routes.

One thing to note is to ensure that server actions do not throw errors, but return the error to the client in a structured format. For example, you can return {success: false, error: error.message}  since this would be safer than throwing the error in a client component.

You should only consider creating api routes when you need to deal with third-party services such as SSO Auth, external APIs for services like Stripe, Webhooks or when implementing real-time features like streaming responses.

Key Takeaways

  • Prefer Server-Side Data Fetching

    • Fetch data on the server whenever possible for better performance, security, and reduced client-server waterfalls.

    • Pass server-fetched data to client components only when needed.

  • Avoid Fetching Data in Root Layouts

    • Fetching data in the layout makes the whole app dynamic, hurting performance.

    • Instead, fetch data in the specific component (e.g., UserNav) and rely on automatic fetch memoization.

  • Use Caching Wisely

    • Understand Next.js caching layers: request memoization, data cache, route cache, and router cache.

    • Use cache: 'force-cache' for static data, revalidate for periodic updates, and cache: 'no-store' for user-specific data.

    • Always invalidate cache after mutations using revalidatePath or revalidateTag.

  • Leverage Streaming and Suspense

    • Stream UI with React’s <Suspense> or loading.tsx files for progressive rendering.

    • Improves perceived performance by letting users interact with non-blocked parts of the page earlier.

  • Parallelize and Preload Data

    • Avoid sequential fetching when not required.

    • Use Promise.all for parallel requests.

    • Use preload patterns to initiate requests early and prevent waterfalls.

  • Optimize Images

    • Use Next.js <Image> component for automatic optimization.

    • Provide fallback images and blur placeholders for better UX.

    • Handle errors gracefully to avoid broken images.

  • Organize Routes and Layouts

    • Use route groups (e.g., (marketing) vs (dashboard)) for better structure.

    • Create dedicated layouts for different sections like AuthLayout for login pages.

  • Prefer Server Actions Over API Routes

    • Server Actions allow running server-side logic directly from components with 'use server'.

    • Use API routes only when necessary (e.g., third-party services, webhooks, streaming APIs).

  • Performance and Cost Benefits

    • Static pages stay static and can be served from a CDN.

    • Fewer dynamic requests reduce server load and costs.

    • Caching + streaming + parallel fetching together ensure faster, smoother apps.

0
0