#Best approach for a SaaS

1 messages · Page 1 of 1 (latest)

dusty cobalt
#

I’m building a SaaS platform that is private. That means only authenticated users are able to interact with it.

So I’m deciding between using a fully React Server Components approach (with Server Actions, etc), or a hybrid approach; that is regular NextJS with client components that fetch API endpoints once mounted.

Both have tradeoffs but my main considerations are:

First approach:
(Good)

  • With first approach (RSC) I can directly get the data without an additional “round trip” to the API endpoints once mounted in the client.
  • That means also not exposing public endpoints other than the authentication ones.
  • I can use Suspense to achieve a decent UX even when waiting from the server.
    (Bad)
  • For what I’ve exploring and testing, when I revalidate a path after a server action, it will call all data requests even in the layout, which is not optimal.
  • How performant and infrastructure optimal is to call always the server.

On the other hand, with a more “client-oriented” option (fetching from the client with React Query for example):
(Good)

  • Maybe more flexibility? Since I can invalidate queries and reuse cache data from the client if I need it.
  • That may be important because I’ll build dashboards or highly interactive UIs.

(bad)

  • A not so clear workaround on hydrating the server side state into React Query in the client, etc.
  • Losing the advantages of RSC like being able to start fetching even before rendering, not having to maintain a separate public api endpoints, etc.

Can you guide me through this?

cobalt duneBOT
#

Hi @dusty cobalt. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You're asking a great question that many teams face when building private SaaS platforms with Next.js. Both the React Server Components (RSC) approach and a more client-oriented approach (using React Query or SWR) have clear tradeoffs, and your summary is accurate. Here’s guidance based on the official Next.js documentation and best practices:


1. React Server Components (RSC) Approach

Pros:

  • Direct Data Access & Security: Server Components can fetch data directly from your database or internal APIs, keeping sensitive endpoints private and not exposing them to the public internet. This is ideal for private SaaS platforms where security is a priority. You can safely use secrets, tokens, and cookies in Server Components without risk of leaking them to the client (Data Security Guide).
  • No Extra Round Trips: Data is fetched on the server and streamed to the client, reducing client-side waterfalls and improving initial load performance. Suspense boundaries allow you to show loading states while waiting for server data (SPA Patterns).
  • No Need for Public API Endpoints: You can avoid maintaining a separate set of public API endpoints for your app, as data fetching happens server-side (Data Security Guide).

Cons:

#
  • Revalidation Granularity: As you noted, revalidating a path after a Server Action can cause all data requests in the layout to be re-fetched, which may not be optimal for highly dynamic or interactive dashboards. There are ongoing discussions about the granularity of cache invalidation with revalidateTag and revalidatePath (GitHub Issue).
  • Server Load: All data fetching happens on the server, which can increase server load and infrastructure costs, especially for highly interactive UIs with frequent updates.

2. Client-Oriented Approach (React Query/SWR)

Pros:

  • Client-Side Flexibility: Libraries like SWR or React Query provide fine-grained cache control, query invalidation, and revalidation on the client. This is especially useful for dashboards or UIs with frequent, interactive updates (Client-Side Fetching, SPA with SWR).
  • Optimistic Updates & Polling: These libraries support optimistic UI updates, polling, and background revalidation, which can improve perceived performance and interactivity.

Cons:

  • Hydration Complexity: Hydrating server-fetched data into the client cache (e.g., React Query) can be tricky, but SWR now supports using server-provided data as a fallback, making this easier (SPA with SWR).
  • Public API Endpoints: You’ll need to expose API endpoints for the client to fetch data, which means you must secure them properly and may lose some of the security benefits of RSC.

Hybrid Approach: Best of Both Worlds

#

Next.js encourages a hybrid approach, leveraging Server Components for initial data fetching and security, and client-side libraries for interactivity and dynamic updates. For example:

  • Initial Data Fetching: Use Server Components to fetch and render initial data securely.
  • Client-Side Updates: Use SWR or React Query in Client Components for real-time updates, polling, or highly interactive features. You can pass server-fetched data as a fallback to SWR, reducing hydration complexity (SPA with SWR).
  • Server Actions for Mutations: Use Server Actions for secure data mutations from the client, and revalidate only the necessary data (Mutating Data).

Summary Table

Feature RSC Approach Client-Oriented (SWR/React Query)
Security Strong (no public endpoints) Must secure public endpoints
Initial Load Perf Fast, streamed, no waterfall May have client-side waterfall
Interactivity Needs Suspense, less granular Fine-grained, optimistic updates
Cache Invalidation Coarse (route/layout) Fine-grained (per query)
Infrastructure Load Server-heavy More client-side, less server load

Recommendation

For a private SaaS platform:

  • Default to Server Components for initial data fetching and security.
  • Adopt SWR/React Query in Client Components for highly interactive or frequently updated parts of your UI.
  • Use Server Actions for mutations, and be mindful of cache invalidation granularity.
#

This hybrid model gives you the security and performance benefits of RSC, while retaining the flexibility and interactivity of client-side data fetching where needed (SPA Patterns, SPA with SWR, Data Security Guide).

If you have specific needs for cache invalidation or real-time updates, lean more on client-side libraries for those segments.


If you need code examples or more details on a specific pattern, let me know!

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@dusty cobalt Kapa.ai is still learning and improving, please let me know how I did by reacting below.

dusty cobalt
#

Can I use swr without exposing an api?

#

@cobalt dune can I use swr without exposing an api?

cobalt duneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

You can use SWR in a way that does not require exposing a public API endpoint by leveraging SWR’s ability to use server-provided data as a fallback. In Next.js App Router, you can fetch data securely in a Server Component and provide it to SWR via the fallback property in <SWRConfig>. This allows your client components to use SWR for reactivity and caching, while the initial data comes directly from the server—no public API endpoint required.

Here’s how it works:

// app/layout.tsx
import { SWRConfig } from 'swr'
import { getUser } from './user' // server-side function

export default function RootLayout({ children }) {
  return (
    <SWRConfig
      value={{
        fallback: {
          '/api/user': getUser(), // getUser() is called server-side
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}

Then, in your client component, you can use SWR as usual:

'use client'
import useSWR from 'swr'

export function Profile() {
  const { data, error } = useSWR('/api/user')
  // ...
}

In this setup, the initial data is provided by the server, and SWR can still handle client-side updates, polling, and revalidation. No public API endpoint is required for the initial fetch, but if you want SWR to revalidate or refetch, you would need to provide a fetcher function, which typically does require an endpoint. However, for static or infrequently changing data, this pattern avoids exposing an API route SPAs with SWR.

If you need client-side revalidation or updates, you will need to expose an endpoint for SWR to fetch from. Otherwise, for static or server-provided data, you can use SWR without a public API.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@dusty cobalt Kapa.ai is still learning and improving, please let me know how I did by reacting below.

dusty cobalt
#

@cobalt dune can I pass the promise used for fallback as a fetcher?

cobalt duneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

Based on the official Next.js documentation, the promise you use for the SWR fallback is intended to provide initial data for the given key, not to be used as a fetcher function. The fetcher in SWR is a function that is called to fetch or revalidate data, typically by making a network request or similar operation.

In the documented pattern, the promise (e.g., from a server-side function like getUser()) is passed as the value for the fallback key in <SWRConfig>, while the fetcher is a separate function (often a function that fetches from an API endpoint):

<SWRConfig
  value={{
    fallback: {
      '/api/user': getUser(), // Promise for initial data
    },
  }}
>
  {children}
</SWRConfig>

And in the client component:

const fetcher = (url) => fetch(url).then((res) => res.json())
const { data, error } = useSWR('/api/user', fetcher)

The documentation does not mention or show passing a promise as the fetcher. The fetcher must be a function, not a promise. The promise is only for the fallback initial data SPAs with SWR.

So, no, you cannot pass the promise used for fallback as a fetcher—the fetcher must be a function.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@dusty cobalt Kapa.ai is still learning and improving, please let me know how I did by reacting below.

dusty cobalt
#

@cobalt dune but isn’t it the same as passing a promise from server to client, then use “use” hook and suspense?

cobalt duneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

Great question! The two patterns—passing a promise from server to client and using the use hook with Suspense, versus using SWR with a server-provided fallback—are similar in that both allow you to start data fetching on the server and hydrate the client with that data. However, there are important differences:

Pattern 1: Promise + use + Suspense

  • How it works: You fetch data in a Server Component, pass the promise to a Client Component, and use React’s use hook to unwrap the promise inside a Suspense boundary.
  • Example:
    // Server Component
    const postsPromise = getPosts();
    return (
      <Suspense fallback={<div>Loading...</div>}>
        <Posts posts={postsPromise} />
      </Suspense>
    );
    
    // Client Component
    'use client'
    import { use } from 'react';
    export default function Posts({ posts }) {
      const allPosts = use(posts);
      // render posts...
    }
    
  • Behavior: The client component suspends until the promise resolves. No client-side revalidation or caching is provided by default. This is a direct server-to-client data streaming pattern Client Components.

Pattern 2: SWR with Server Fallback

  • How it works: You provide initial data to SWR via the fallback property in <SWRConfig>, typically using a server-side promise. SWR then manages the cache and can revalidate or refetch data on the client if a fetcher is provided.
  • Example:
    // Server Component
    <SWRConfig value={{ fallback: { '/api/user': getUser() } }}>
      {children}
    </SWRConfig>
#

// Client Component
'use client'
import useSWR from 'swr';
const { data } = useSWR('/api/user');

- **Behavior:** The initial render uses the server-provided data, but SWR can revalidate, refetch, and cache data on the client, providing more interactivity and flexibility [SPAs with SWR](<https://nextjs.org/docs/app/guides/single-page-applications#spas-with-swr>).

---

### **Key Differences**
- **Reactivity:** The `use` + Suspense pattern does not provide client-side revalidation or cache updates. SWR does.
- **Caching:** SWR manages a client-side cache and can refetch data as needed. The `use` + Suspense pattern does not.
- **API Exposure:** Both patterns can avoid exposing public APIs for the initial data load, but SWR requires an endpoint if you want to revalidate or refetch on the client.

---

**In summary:**  
While both patterns start with a server-provided promise, using SWR adds client-side caching, revalidation, and more flexibility for interactive UIs. The `use` + Suspense pattern is more direct and simple, but less dynamic after the initial render.

If you need only initial data hydration, they are similar. If you need ongoing updates or interactivity, SWR offers more features [SPAs with SWR](<https://nextjs.org/docs/app/guides/single-page-applications#spas-with-swr>), [Client Components](<https://nextjs.org/docs/app/getting-started/fetching-data#client-components>).
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@dusty cobalt Kapa.ai is still learning and improving, please let me know how I did by reacting below.

dusty cobalt
#

@cobalt dune but based on that, can I pass then the promise but instead of “use” use it as a fetcher for swr?

cobalt duneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

No, you cannot use a promise as the fetcher for SWR. According to the official Next.js documentation, the fetcher in SWR must be a function, not a promise. The promise is used for the fallback value in <SWRConfig>, which provides the initial data for a given key. The fetcher is a function that SWR will call to fetch or revalidate data, typically by making a network request.

Here’s the documented pattern:

<SWRConfig
  value={{
    fallback: {
      '/api/user': getUser(), // Promise for initial data
    },
  }}
>
  {children}
</SWRConfig>

And in your client component:

const fetcher = (url) => fetch(url).then((res) => res.json())
const { data, error } = useSWR('/api/user', fetcher)

The documentation does not mention or show passing a promise as the fetcher. The fetcher must be a function, not a promise. The promise is only for the fallback initial data SPAs with SWR.

So, you cannot pass a promise as the fetcher for SWR—the fetcher must be a function.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@dusty cobalt Kapa.ai is still learning and improving, please let me know how I did by reacting below.

dusty cobalt
#

@cobalt dune cuál sería el approach más apropiado para mi plataforma?

cobalt duneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

Para una plataforma SaaS privada donde solo usuarios autenticados pueden interactuar, la documentación oficial de Next.js recomienda un enfoque híbrido que combine los beneficios de los Server Components (RSC) y el data fetching en el cliente con librerías como SWR o React Query, según las necesidades de cada parte de tu aplicación.

Recomendación según la documentación:

  • Usa Server Components para el acceso inicial a datos sensibles y autenticación:
    Los Server Components te permiten acceder directamente a la base de datos o APIs internas sin exponer endpoints públicos, lo que es ideal para la seguridad en una plataforma privada. Además, puedes aprovechar Server Actions para mutaciones seguras y controlar el acceso a los datos según el rol del usuario (Data Security Guide, Server Components).

  • Utiliza Client Components y SWR/React Query para interactividad y dashboards dinámicos:
    Para secciones altamente interactivas, como dashboards, puedes usar Client Components junto con SWR o React Query. Esto te da flexibilidad para revalidar, cachear y actualizar datos en tiempo real en el cliente. Puedes hidratar el estado inicial desde el servidor usando el fallback de SWR, evitando así problemas de hidratación y mejorando la experiencia de usuario (SPAs con SWR).

#
  • Evita exponer endpoints públicos innecesarios:
    Si solo necesitas el dato inicial y no requieres revalidación en el cliente, puedes pasar los datos desde el servidor usando el fallback de SWR, sin necesidad de exponer un API público. Si necesitas revalidación, sí tendrás que exponer un endpoint seguro (SPAs con SWR).

  • Gestiona la seguridad y la autorización en el servidor:
    Centraliza la lógica de acceso a datos y autorización en un Data Access Layer (DAL) en el servidor, asegurando que solo se exponga la información mínima y necesaria al cliente (Data Access Layer).


En resumen:
El enfoque más apropiado para tu plataforma es un modelo híbrido:

  • Server Components para seguridad, acceso inicial a datos y lógica sensible.
  • Client Components + SWR/React Query para interactividad y actualizaciones dinámicas donde lo requieras.

Este modelo te da lo mejor de ambos mundos: seguridad, performance y flexibilidad para construir interfaces ricas e interactivas, siguiendo las mejores prácticas recomendadas por Next.js.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: