AI Unified Process plugin for the NestJS/Drizzle + Next.js stack
92
91%
Does it follow best practices?
Impact
97%
1.15xAverage score across 3 eval scenarios
Passed
No findings from the security scan
This is a lookup used by /implement, /drizzle-migration, /nest-test, /react-test, and
/playwright-test before writing any code. Its job is to answer one question: where do this
project's two applications live, and which of its conventions must new code match?
Never assume — always run this detection first. Two of the answers are unforgiving:
.js suffix on
every relative import even though the source file is .ts. Omit it and the build fails; add it
in a project that isn't NodeNext and the build fails the other way.src/pages
directory in an App Router project is not inert — Next.js will try to route it.The rest are less dramatic but produce code that reads as foreign to the project: queries in the wrong layer, types duplicated instead of shared, pages split across two conventions.
| # | Question | Signal | Consequence if wrong |
|---|---|---|---|
| 1 | API app root | The workspace whose package.json has @nestjs/core in dependencies | Code lands in the wrong app |
| 2 | Web app root | The workspace whose package.json has next in dependencies | Code lands in the wrong app |
| 3 | ESM/NodeNext | "type": "module" in the API's package.json and "module": "NodeNext" (or "Node16") in its tsconfig.json | Missing .js suffixes; nothing compiles |
| 4 | Drizzle config | drizzle.config.ts in the API root — read schema and out | Schema edits in the wrong file; migrations in the wrong directory |
| 5 | Router style | src/app/ present → App Router; src/pages/ present → Pages Router | A second conflicting router |
| 6 | Route indirection | Whether existing src/app/**/page.tsx files hold the page markup or re-export a component from elsewhere | Convention split across the codebase |
| 7 | Shared contract package | A workspace package imported by both apps that exports request/response types | Duplicated, drifting types |
Read the repo-root package.json and look for workspaces. Expand each glob and read every
matched package.json to answer questions 1 and 2.
node -e "console.log(require('./package.json').workspaces)"A monorepo commonly puts the two apps at apps/api and apps/web, but the names are arbitrary —
resolve them from the dependency signals, not from the directory names.
If there is no workspaces field, the two applications may be separate repositories or plain
sibling directories. Search for nest-cli.json and next.config.* instead. State which roots
you found before writing anything, so a wrong guess is visible immediately rather than after a
dozen files have landed in the wrong place.
node -e "const p=require('./<api>/package.json'); console.log(p.type)"
grep -E '"module"|"moduleResolution"' <api>/tsconfig.json"type": "module" together with "module": "NodeNext" means every relative import specifier
ends in .js:
import { ProductsService } from './products.service.js'; // correct — source is .ts
import { ProductsService } from './products.service'; // fails to resolve at runtimeThe quickest confirmation is the project's own code: open any existing file with a relative import
and copy what it does. If existing imports carry .js, yours must too.
Read drizzle.config.ts in the API root. Two fields matter:
schema — the file to edit when the entity model changes (commonly ./src/database/schema.ts)out — the directory generated migrations land in (commonly ./drizzle/migrations)Never infer either from convention. A project that keeps its schema split across several files
under a schema/ directory is normal, and writing into a single schema.ts that the config does
not point at produces a table that never reaches the database.
src/app/ means App Router. Then check what a route file actually contains:
// Direct — the route file holds the page
export default function ProductsPage() {
return <main>…</main>;
}// Indirect — the route file is a thin wrapper
'use client';
import { ProductsPage } from '../../views/ProductsPage';
export default function Page() {
return <ProductsPage />;
}Where the project uses indirection, new pages follow it: a thin wrapper at the route, the markup in
a component beside its siblings. This matters beyond /implement — /react-test must target the
component that holds the markup, because a test rendering the wrapper asserts nothing.
Note that a directory named views (or screens, or containers) is a deliberate choice to avoid
src/pages, which the Pages Router would claim. Do not "tidy" it into src/pages.
Find one already-implemented feature and copy its exact shape rather than generating from this table in isolation. The table tells you where things live; an existing feature tells you how this team writes them.
*.repository.ts, or do features
consume shared repositories exported by a core module? Match whichever exists — importing a shared
repository where one exists is correct; duplicating its queries into a new file is not.dto/, or imported from a shared
contract package? If a shared package exists, use it; the whole point is that both halves of the
stack change together.apiGet/apiPost or similar) and a
hook wrapping it? Use them. A bare fetch in a project that has a client module bypasses its
error handling and base-path logic.If the project has no implemented feature to copy, fall back to these documented defaults rather than inventing a structure:
*.repository.ts inside the feature folder.dto/ directory.src/app/**/page.tsx, with no separate view directory.fetch against relative /api/... paths.Say which defaults you applied, so the first feature's conventions are a visible decision rather than an accident the rest of the codebase then inherits.
src/app/<route>/page.tsx (web — thin wrapper, or the page itself)
→ <view component> (web — markup, state, data fetching)
→ fetch / apiGet('/api/<resource>')
⇢ rewrite in next.config.ts ⇢ http://<api-host>/api/<resource>
<Feature>Controller (api — routing, DTO binding; no logic)
→ <Feature>Service (api — orchestration; throws domain errors)
→ <Feature>Repository (api — every Drizzle query lives here)
→ schema.ts (api — the tables, owned by /drizzle-migration)
← <Feature>Response (api — mapped shape, never a raw row)Never let a Drizzle query escape the repository, and never let a raw database row reach the controller's return type — those two boundaries are what make the backend testable in two tiers.