Nfs architecture patterns
Skill juncoding/nextjs-fullstack-starter/skills/nfs-architecture-patterns
Claude Code plugin: scaffold and maintain lightweight back-office apps on pure Next.js (App Router) — Server Components for reads, Server Actions for writes, services in src/server/modules/. No tRPC.
npx -y skills add juncoding/nextjs-fullstack-starter --skill nfs-architecture-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Reference patterns for ongoing development on a Next.js fullstack project scaffolded with nextjs-fullstack-starter — Server Components for reads, Server Actions for writes, services in src/server/modules/. Use this whenever adding a new module, writing a new page or Server Action, deciding between Server Action vs route handler vs MCP tool, wiring permissions, structuring services, handling errors, caching with cacheTag / updateTag, or making any architectural decision in a project that was bootstrapped with this plugin. Triggers on phrases like 'add a new module', 'create a Server Action', 'where should this logic go', 'follow project conventions', 'how do I invalidate the cache', 'should this be a page or an action', or any 'how do I do X in this project' question.
SKILL.md
9.1 KB, as published. Nobody here has run it
Architecture patterns for ongoing development
For projects already scaffolded with nextjs-fullstack-starter. Explains the patterns to follow when adding features. Companion to the nfs-scaffold-app skill which only handles initial setup.
Use this skill when
- Adding a new business module (e.g.
customer,order,invoice). - Writing a new page, Server Action, or route handler.
- Deciding whether something belongs in a page, a Server Action, a route handler, an MCP tool, or a cron job.
- Wiring permissions on a new resource.
- Structuring a service that touches multiple modules.
- Caching with
cacheTag/updateTagand not sure when to use which. - Handling errors at any layer.
- Reviewing whether a PR follows project conventions.
The four-rule cheat sheet
src/app/is a thin delivery layer. No business logic. No DB queries. Just: validate, call a service, return.src/server/is the entire backend. Every file starts withimport "server-only";.- Permissions live in services. Every service method touching user-owned data takes
userIdfirst and callsrequirePermission. - Audit calls live in services, inside the same transaction as the mutation.
If you remember nothing else, remember these four.
Reference index
Read the file matching your task:
| Doing this... | Read this |
|---|---|
| Creating a new business module (service, schema, types) | references/service-layer.md |
| Writing a Server Component page that reads data | references/server-components-and-pages.md |
| Writing a Server Action for a write/mutation | references/server-actions.md |
| Caching reads, invalidating after writes | references/caching.md |
| Wiring auth / RBAC for a new resource | references/permissions-and-audit.md |
| Adding a REST endpoint (webhook, third-party callable, file download) | references/route-handlers.md |
| Throwing / catching errors at any layer | references/error-handling.md |
The delivery-layer matrix
When you have new functionality, decide which delivery layer it lives in:
| Caller | Delivery layer |
|---|---|
| The app's own UI — reading data | Async Server Component page in src/app/(dashboard)/<feature>/page.tsx → service |
| The app's own UI — writing data | Server Action in src/server/actions/<feature>.actions.ts → service |
| An AI client (Claude Desktop, Cursor) | MCP tool in src/server/mcp/tools/ (wraps the same service) |
| A webhook (Stripe, Resend, Svix-signed) | Route handler at src/app/api/webhooks/<provider>/route.ts |
| A scheduled job | Cron registration in src/server/jobs/, kicked by instrumentation.ts |
| A third-party that needs REST | Route handler at src/app/api/v1/<resource>/route.ts |
| File upload / download | Route handler (Web Streams API) |
| A test | Direct service call with mocked Prisma, or createCaller-style test harness if you build one |
All of these end up calling the same service method — only the wrapper layer differs.
Server Component vs. client component
Default: Server Component. Add "use client" only when you need:
- React hooks (
useState,useEffect, etc.) - Browser-only APIs (
window,document,localStorage,IntersectionObserver) - Event handlers (
onClick,onChange,onSubmit— though<form action>works without client JS) - Third-party libraries that explicitly need a client (cmdk, framer-motion, etc.)
When you need client interactivity over server-fetched data, fetch on the server and pass data in as a prop:
// page.tsx — Server Component, fetches data
import { CustomerFilters } from "./_components/filters"; // client component
export default async function Page() {
const session = await requireSession();
const customers = await customerService.list(session.user.id, {});
return <CustomerFilters initial={customers} />;
}
Don't fetch data inside client components by spinning up a route handler just to feed them — that's reintroducing the JSON layer you came here to avoid.
Server Action vs. route handler
Both are POST handlers. Pick by caller:
| Server Action | Route handler | |
|---|---|---|
| Caller | The app's own UI (forms / buttons) | Anything else — webhooks, mobile, AI, scripts |
| URL | None — invoked by reference | Real URL — /api/... |
| Body shape | FormData or any serializable JS value | Arbitrary HTTP body |
| Use revalidate? | Yes — revalidatePath / updateTag after the mutation | No — the caller manages their own state |
| Best for | Forms, button-click mutations, anything triggered by the UI | Anything triggered by something external |
If you find yourself writing fetch("/api/customers", { method: "POST" }) from inside the app's own client component, stop — that's the Server Action's job. The fetch + JSON + handler pattern undoes the type-safety you came for.
Anti-patterns to refuse
- DB queries in pages or Server Actions. The page is delivery; Prisma is service. If a page has
db.customer.findMany, move it to the service. - Permission checks in pages or Server Actions. Same reason — easy to forget, security-critical, belongs with the data layer. Pages check
requireSession; services checkrequirePermission. - Server Actions that do business logic inline. The action validates and calls; the service does the work. If your action body is >20 lines, the logic belongs in a service.
- Calling
fetch('/api/...')from a client component when a Server Action exists. Server Actions exist for this exact case. Use them. revalidateTag/revalidatePathinside services. These are page-side invalidation primitives (call them from Server Actions, not services). Inside services, useupdateTagfor tag-keyed invalidation. The distinction matters in Next.js 16 — seereferences/caching.md.- Importing client libraries (React Query, zustand, etc.) into Server Components. They'll crash at build time, but more subtly: they signal that someone is trying to manage server-fetched state in the client when the page should just refetch.
use serverdirective at the top of apage.tsxor component file. That makes EVERY export a Server Action, which is almost never what you want. Server Actions go in dedicated*.actions.tsfiles insrc/server/actions/.
When to break the rules
The rules exist because they pay rent — they make the codebase navigable, secure, and refactorable. Breaking them is allowed when the break itself is the cheaper option, and you're explicit about it.
Examples of legit breaks:
- A service method that reads but doesn't mutate and is called from a public, unauthenticated route handler (e.g. a supplier portal). The
userId-first signature is awkward there — use a sentinel or acceptnulland document why. - A read trivially hot enough to inline in a Server Component (e.g. a header count). Add a comment, move on.
- A
'use server'action file co-located next to a page (_actions.tsinstead ofsrc/server/actions/) when the action is genuinely page-local and won't be reused. Fine, but think about whether reuse will sneak in.
When you break a rule, leave a one-line comment explaining why. Future you, or the next Claude session, needs to know it was intentional.
Workflow — adding a new module
A typical "add a customer module" session looks like:
- Prisma model. Add
Customertoschema.prisma. Migrate. - Seed permissions. Add
customers:read,customers:writeto your permission seed, attach to relevant roles. - Service module folder.
src/server/modules/customer/—.service.ts+.schema.ts. Copy the shape from_example/. - Server Actions.
src/server/actions/customer.actions.ts— create / update / delete actions, each oneawait requireSession()thencustomerService.<method>(session.user.id, ...). - Pages.
src/app/(dashboard)/customers/—page.tsx(list),[id]/page.tsx(detail),new/page.tsx(form),[id]/edit/page.tsx(edit form). Each one async, calls the service. - Wire sidebar. Add the Customers entry in
src/components/layout/sidebar.tsx. - Tests.
customer.service.spec.tscovers the service. See thenfs-testing-patternsskill. - Run the verification gate.
pnpm verify— must be green before commit.
The module is a known shape. Don't reinvent it.