Fetching Data Directly from a Database in a Server Component

Server Components can query a database directly, keeping credentials off the client. Learn the pattern and how to deduplicate repeated queries.

7 min read

Fetching data directly from a database in Next.js means a Server Component runs the query itself instead of calling an HTTP API. Because the component runs on the server, the database credentials never reach the client, and the query result renders straight into the HTML the user receives.

Direct database access removes a network hop. There is no separate API server and no fetch from the browser, just a query that runs during rendering. The query result is then serialized into HTML, so the browser only ever receives the final markup.

typescripttypescript
// app/lib/posts.ts
import "server-only";
import { cache } from "react";
import { db } from "@/lib/db";
 
export const getPosts = cache(async () => {
  return db.query.posts.findMany();
});

The db import is a database client created once in its own module. The server-only package stops this file from being imported into a Client Component by accident, and React cache makes repeated calls within one request return the same result.

App.tsxApp.tsx
// app/posts/page.tsx
import { getPosts } from "@/lib/posts";
 
export default async function PostsPage() {
  const posts = await getPosts();
  return <ul>{posts.map((post) => <li key={post.id}>{post.title}</li>)}</ul>;
}

The page awaits the query and renders the rows directly. Nothing about the database reaches the browser, because only the rendered HTML is sent. A slow query here only delays this page, and it can be paired with a loading file to show a fallback while the query runs.

That guarantee holds only while the rows stay on the server. The moment you pass a row to a Client Component as a prop, every column in it is serialized into the page, so select the fields the UI needs or map the row to a smaller object first.

Why query the database from the component

Fetching through an API layer makes sense when the data comes from another service. When you own the database, querying it directly is simpler: one round trip instead of two, and no serialized API contract to maintain. It also avoids the duplicated validation and error handling that an API layer adds between the component and the data.

Direct access also keeps secrets safe. The connection string lives in an environment variable that stays on the server, since only variables prefixed with NEXT_PUBLIC_ are exposed to the browser.

Any ORM or query builder works here, as long as the client itself is created in a server-only module. Keep the client construction in a separate module so no component ever sees the connection string directly.

ORM or raw driver

The pattern is the same whether you use Prisma, Drizzle, or a raw driver. Create the client once in a server-only module, export a cached query function, and call it from the component. The React cache wrapper is what gives you per-request deduplication regardless of the tool.

Deduplicating repeated queries

A layout and a page that both call getPosts would run the query twice without the cache wrapper. React cache memoizes the function per request, so the second call reads the first result.

Fetch calls are deduplicated automatically by Next.js; database calls need this manual wrapper as the equivalent. For the full comparison with fetch memoization, see request deduplication vs React cache. For the fetch-based approach, see data fetching in Server Components.

Rune AI

Rune AI

Key Insights

  • Server Components can query a database directly.
  • Credentials stay on the server; only HTML reaches the client.
  • The server-only package blocks accidental client imports.
  • Wrap queries in React cache to deduplicate within a request.
  • Direct access removes one network hop compared to an API layer.
RunePowered by Rune AI

Frequently Asked Questions

Is it safe to query a database from a Server Component?

Yes. The component runs on the server, so credentials never reach the client. Only the rendered HTML is sent.

Do I still need an API route?

Only when the data comes from another service or must be shared with clients directly. For a database you own, querying directly is simpler.

Why wrap the query in React cache?

So a layout and a page that call the same query within one request run it once instead of twice.

Conclusion

A Server Component can query a database directly, keeping credentials on the server and skipping a network hop. Wrap the query in React cache so repeated calls within one request share a single result.