One concept, four names, and how to stop that
In a single-file script a bad name costs you a few seconds. In the Kirana Store the same concept exists in a Postgres column, a Prisma model, a shared type, a NestJS DTO, a JSON payload, a React prop and a form field — and each of those is somewhere a different name can creep in.
Here is the shape of the problem, written out:
Postgres total_paise
Prisma totalPaise
API response total
React prop amount
Form field price
Nothing is broken. Everything runs. And now a reader tracing a wrong invoice has
to hold four synonyms in their head, grep finds a quarter of the uses, and the
one place that quietly converts total to rupees is invisible because the name
does not change when the unit does.
The rule for a full-stack codebase: one concept, one name, all the way through. Not "a consistent convention per layer" — the same word.
Casing is the only thing allowed to change
The exception, and the only one:
Postgres column total_paise snake_case, because SQL is
Prisma field totalPaise camelCase, because JavaScript is
Prisma maps between them for you, which is the point of @map:
model Order {
id String @id @default(cuid())
totalPaise Int @map("total_paise")
placedAt DateTime @map("placed_at")
@@map("orders")
}
The word is identical. total_paise and totalPaise are the same name in two
alphabets, and anybody can see that. total_paise and amount are not.
The unit travels with the name
This is the single highest-value naming habit in a money application, and the Kirana Store applies it without exception:
export type Paise = number;
export function formatPaise(paise: Paise): string { … }
export function rupeesToPaise(rupees: number): Paise { … }
totalPaise, ratePaise, amountPaise, deliveryFeePaise. Never total,
never amount, never price.
Two reasons, and the second is the real one.
The obvious reason is that a reader knows the unit. The better reason is that the name changes at exactly the point the value changes, so the conversion is impossible to hide:
const totalPaise = cart.items.reduce((sum, i) => sum + i.pricePaise * i.quantity, 0);
const displayed = formatPaise(totalPaise);
If somebody writes total / 100 and assigns it to something still called
totalPaise, the line looks wrong on sight. If both were called total, it
looks fine, and the bill is a hundred times out.
The same applies to time. createdAt is an instant; deliveryDate is a calendar
date. Those are different types with different bugs, and module 13 exists because
we conflated them once.
The shared package is the single source of naming truth
packages/shared is not only about types. It is the place the name is decided
once, so neither side can drift:
export interface OrderSummary {
orderNumber: string;
status: OrderStatus;
totalPaise: Paise;
placedAt: string; // ISO instant
deliverySlot: SlotId;
}
The API returns that, the React component receives that, and if somebody renames
totalPaise the build breaks in both apps at once. A rename that cannot be
done in one place is a rename nobody will do, which is how a codebase ends up
with total, amount and totalPaise all meaning the same thing and none of
them being safe to delete.
Defining the same shape twice — once in the API, once in the web app — is the thing this course set up a monorepo to avoid. Two copies agree until the day they do not, and that day is the day of a production bug that typechecks on both sides.
Names that carry meaning, not implementation
// implementation in the name
const orderArray = await this.prisma.order.findMany();
const userMap = new Map<string, User>();
const dataString = JSON.stringify(order);
TypeScript already says all three. orders, usersById, payload.
usersById is worth pausing on: for a Map or a keyed object, the name should
say what the key is. usersById, stockByProductId, slotsByDate. Then
usersById.get(orderId) reads as the bug it is.
Booleans read as questions
if (order.delivered) … // is it?
if (order.isDelivered) … // yes
if (!order.cancellable) … // hmm
if (!canCancel(order)) … // clear
is, has, can, should. And never a negative name: isNotPaid gives
you if (!order.isNotPaid), which nobody reads correctly at speed.
Layer-specific names for layer-specific things
The counterweight. Some names should differ, because the things differ:
Order // the database row, with everything
OrderSummary // what a list endpoint returns
CreateOrderDto // what a client is allowed to send
OrderCardProps // what one React component needs
Four names for four genuinely different shapes. This is not drift — it is
precision, and the suffix says which one you are holding. The mistake would be
calling all four Order and then wondering why the list endpoint returns a
customer's address.
The test: do these two names describe the same set of fields with the same meaning? If yes, one name. If no, two names — and the difference should be in the name, not in the reader's memory.
Files and routes
apps/api/src/orders/orders.service.ts
apps/api/src/orders/orders.controller.ts
apps/web/app/(shop)/orders/page.tsx
Feature folders, matching names across the two apps, and the URL segment is the
same word again. Somebody who knows the Kirana Store has an orders concept can
find every part of it without asking, and a grep -r orders is a complete
answer.
utils.ts, helpers.ts and common.ts are where code goes to be lost. If
something is genuinely general, it is general about something: money.ts,
dates.ts, pagination.ts — which is precisely what packages/shared/src
contains.
Check your work
The rule: one concept, one name, from the column to the prop.
The one thing allowed to change: casing, because SQL is snake_case and
JavaScript is camelCase. Prisma's @map makes that explicit.
Why the unit belongs in the name: so the name changes at the moment the value does, which makes a hidden conversion visible.
Why the shared package matters for naming: a rename happens in one place, so it happens at all.
Why not orderArray: the type already says it.
What a keyed collection's name should say: what the key is —
stockByProductId.
Boolean naming: is/has/can/should, and never negative.
When four names for one concept are right: when they are four different
shapes — Order, OrderSummary, CreateOrderDto, OrderCardProps.
Practice
- Pick one concept in your Kirana Store and list every name it has, from the Postgres column to the React prop. Count the distinct words.
- Find one place where a name changes across a layer boundary for no reason. Rename it and see how many files the compiler makes you touch.
- Find a money value whose name does not carry
Paise. Rename it, then look for a conversion you had not noticed. - Find a type defined twice — once in the API, once in the web app. Move it to
packages/sharedand import it in both. - Rename a field in
packages/sharedand confirm both apps fail to build. - Find a
Mapor keyed object and rename itxByY. - Find a boolean not named as a question. Fix it.
- Search for
utilsorhelpersin your repo. For each thing inside, name the file it belongs in. - Write down the difference between
OrderandOrderSummaryin your own codebase. If you cannot, one of them is wrong. grep -rnone feature word across the monorepo. If it does not find the front end, the API and the schema, the naming has drifted.
Next: which layer a rule belongs to.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship