Writing database queries directly in every single page is fast at first, but once you need to change a query, add permissions, or write tests, you end up with duplicated code everywhere. A thin data layer pays off once the project grows.
The mental model
Put use-case functions like list, detail, and create in lib/data/notes.ts. Importing the server-only package means a Client Component that accidentally imports it fails fast with a build error. Instead of sending the entire ORM result to the page, select only the fields you need. Follow the ORM's official Next.js pattern for reading the connection string from an environment variable and storing it on a global object.
Let's build it together
// lib/data/notes.ts
import "server-only";
import { db } from "@/lib/db";
export async function getPublishedNotes() {
return db.note.findMany({
where: { published: true },
select: { id: true, title: true, updatedAt: true },
orderBy: { updatedAt: "desc" },
});
}
export async function getNoteById(id: string) {
return db.note.findUnique({ where: { id } });
}How the code works
getPublishedNotes selects only published records and returns just the three fields the UI needs. The page can call this function without knowing anything about the database implementation.
The UI can get typed note data from the data layer without knowing any database details.5-Minute Try-It
Write a getNotesByUser(userId) function that returns an error instead of querying when userId is missing.
Next.js — Fetching Data — Next.js