Style
Litestore's code follows conventions for naming, imports and file layout.
Litestore's code follows a set of conventions for naming, imports and file layout. Following them keeps your changes consistent with the rest of the codebase.
Enforcement
The conventions are written down in .claude/guides/code-style.md, .claude/guides/style-guide.md and .claude/CLAUDE.md. Biome checks formatting, import order and its recommended lint rules, and tsc checks the types. Both run in bun run verify and in CI.
The rest of the conventions below can't be checked by a tool, so they rely on code review and on the guides, which AI coding agents read before they change the code.
Functions and naming
Write pure functions as function declarations. Write server actions as arrow functions built with the module's permission wrapper.
// Pure functions - use function declarations
export function calculateDiscount(price: number, percent: number): number {
return price * (1 - percent / 100)
}
// Server actions - use arrow functions with the module permission wrapper
export const createOrder = orderPermission("create")
.createServerAction()
.input(orderSchema)
.handler(async ({ input }) => {
return db.order.create({ data: input })
})Each kind of operation gets one verb. The guide spells this out because the codebase had drifted until reads were named get or find about equally often.
| Operation | Prefix | Examples |
|---|---|---|
| A database read that returns entities | find* | findProduct, findAdminCampaigns |
| A computed or config value | get* | getDashboardViewModel, getDefaultCurrency |
| A call to an outside service | fetch* | fetchShopifyProducts |
| A write | create*, update* or delete* | Don't use add, edit, remove, insert or set. |
| A boolean | is*, has* or can* | — |
| A conversion | to* or from* | toCents, fromDecimal |
A generic name needs either a domain in front of it or a single shared home.
If every module defines its own getProvider, getPlatformConfig or isFeatureEnabled, the copies shadow each other and grep can't tell them apart.
Core modules also avoid vendor names. The guide's example replaces an IMPORT_SOURCES map keyed by shopify/woocommerce/tiktok with a generic SalesChannel const.
Name a component's props type after the component. Don't write a bare type Props =. Write type CouponDetailProps instead.
Types, enums and constants
Write string enums as const objects with an inferred type, not as a TypeScript enum:
export const ProductStatus = {
Draft: "Draft",
Published: "Published",
Scheduled: "Scheduled",
} as const
export type ProductStatus = (typeof ProductStatus)[keyof typeof ProductStatus]Put a type in the same file as the code it describes. A zod schema and its z.infer type go in the same file. There is no types/product.ts next to schema/product.ts.
Before you define a type, the guide asks you to grep modules/, platform/ and ui/ for an existing one and import it.
Prisma enums follow a stricter rule. If a value can be worked out from timestamps, relationships, events or policy, it must not be a writable enum. Instead, one canonical resolver works it out, such as modules/reviews/review-lifecycle.ts or deriveLifecycleStatus in modules/customers/lifecycle.ts.
Enums that describe stable categories in the domain stay as enums:
DiscountType
PaymentTransactionType
FinancialStatus
UserRole
InventoryPolicy
CampaignVisibilityImports and the ~/ alias
tsconfig.json maps ~/* to the root of the repository. You import every shared module through this alias instead of a relative path.
"paths": {
"~/*": ["./*"],
"~/.source/*": ["./.source/*"],
"content-collections": ["./.content-collections/generated"]
}import { db } from "~/platform/db/client" // Path alias
import { cx } from "~/platform/utils/cva" // Class merging (not cn)
import { Icon } from "~/ui/icon" // Sprite iconsFollow these three import conventions.
| Convention | What you do |
|---|---|
Use cx from ~/platform/utils/cva | You use cx, not cn. The repository has no cn helper. |
| Use icons from the sprite | Don't write import { Store } from "lucide-react". You write <Icon name="lucide/store" /> from ui/icon.tsx instead, and scripts/build-icons.ts compiles the sprite from the SVG sources in assets/. No file imports lucide-react directly. |
| Don't re-export through barrels | Don't write export * from. With named exports, grep finds where something comes from. |
Imports also follow a direction. Storefront code doesn't import admin code and the other way round, module rules don't import from a module's admin/ or storefront/ folder, client code doesn't import server-only modules, and nothing imports from app/.
File layout
A domain keeps all the code for one business area in one folder:
modules/products/
├── pricing/ … # Rules and services: no "use server", no React
├── admin/ # Everything the admin uses
│ ├── actions.ts # Permission-wrapped server actions (entry points)
│ ├── crud.ts # BaseCRUD extension
│ ├── queries.ts # Read-only queries
│ ├── schema.ts # Zod schemas, nuqs parsers
│ ├── payloads.ts # Prisma select/include types
│ └── columns.tsx, table.tsx, product-form.tsx, …
└── storefront/ # Everything the shop uses (public actions, queries, UI)Under app/, a route folder only contains the route files that Next.js reads:
page.tsx
layout.tsx
loading.tsx
error.tsx
route.tsDon't use private folders such as _components/, _lib/, _hooks/ or _utils/. Put route UI in modules/<area>/admin/ or modules/<area>/storefront/, and rules in the module's root.
Keep route files thin. If a route's page.tsx maps over data to render JSX, move that markup into a component.
Comments
Comments follow the same idea of leaving out what isn't needed.
Don't write a comment that starts with a verb and just repeats what the next line does. A comment is worth keeping when it states a constraint, which usually means it contains one of these words:
because
must
race
stale
idempoten
atomic
lockLeave out a one-line JSDoc block such as /** Get the default currency */ above a symbol with the same name. Keep JSDoc that adds @param details or a security, cascade or invariant note.
Type casts
Avoid as any and as unknown as. A cast tells the compiler to stop checking, so a mistake in money, order or inventory code can reach production without a type error. Biome's noExplicitAny rule is turned off, so nothing stops you from writing a cast, and reviewers should ask for a typed alternative. Usually that means a type guard, a zod parse at the boundary or a Prisma payload type.
Conventions by area
The guides group the remaining conventions by the problem they prevent.
Backwards compatibility
When you rename something, delete the old name in the same change. The guide lists the leftovers that you must not keep after a rename:
Type aliases (export type OldName = NewName)
Function aliases (export const oldFn = newFn)
Re-exports for renamed modules
Deprecated parameter support
Legacy API endpointsThe codebase has no export * from statements. The guide also rejects renaming an unused variable to _unusedVar. Delete the code instead.
Related
Continuous integration
CI runs a set of checks on every push, and the repository gives AI coding agents guides to follow.
Guides
Each guide in .claude/guides/ covers one kind of change, such as a page, a model or a module.
Linting
Biome formats and lints the codebase, and one command covers everything to run before you push.