Versioning, and not breaking your own front end
The moment a front end depends on your API, changing it can break something. This lesson is about which changes are safe, how to make the unsafe ones, and why you probably need less versioning machinery than you think.
Safe and unsafe changes
Safe — no version needed:
- Adding a new endpoint
- Adding an optional request field
- Adding a field to a response
- Adding a new value to an enum the client only displays
- Relaxing a validation rule — raising a maximum length
- Making a required request field optional
Breaking — something will stop working:
- Removing or renaming a response field
- Removing or renaming an endpoint
- Adding a required request field
- Changing a field's type —
stock: 12tostock: "12" - Changing the meaning of a field, without changing its name
- Tightening validation
- Changing a status code for the same situation
- Adding an enum value the client switches on
The pattern: adding optional things is safe; removing, renaming and requiring are not.
Two on that list are worth dwelling on.
Changing meaning without changing the name is the worst kind, because
nothing fails. If total stops including delivery, every client keeps working
and every number is wrong. Rename the field when the meaning changes — a
compile error is far better than a silent one.
Adding an enum value breaks clients that switch on it. A new order status
AWAITING_PICKUP hits the default branch of every client switch. Either
design clients to handle unknown values from the start, or treat it as
breaking.
Versioning in NestJS
// apps/api/src/main.ts
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: "1",
});
@Controller({ path: "products", version: "1" })
export class ProductsV1Controller { ... }
@Controller({ path: "products", version: "2" })
export class ProductsV2Controller { ... }
Giving /api/v1/products and /api/v2/products.
URI versioning is the most common and the most visible — you can see which version a request used in any log. Header versioning is tidier in theory and harder to debug, because a URL alone no longer tells you what was called.
Version the whole API, not individual endpoints. /api/v1/products and
/api/v2/orders in the same application is confusing to consume.
When you actually need it
Being honest: for the Kirana Store, probably never.
Versioning exists because you cannot update every client. That applies when:
- A mobile app is installed and old versions are still running
- Third parties integrate with you
- You have paying API customers
None of those is true when your only client is a Next.js app you deploy at the same time. You can change both halves in one commit, which is exactly what the shared types in module 9 are for.
Do not add versioning because it looks professional. Two versions means maintaining two code paths, testing both, and deciding when the old one dies. That is real work, and unnecessary work is not professional.
What to do instead: keep the ability to version — enable it, put everything
under v1 — and use it only when something outside your control depends on the
API. Starting at v1 costs nothing and saves an awkward migration later.
Making a breaking change without a version
Most breaking changes can be avoided with a transition.
Renaming a field — three deploys:
// 1. Add the new name, keep the old
return { pricePaise: v.pricePaise, price_paise: v.pricePaise };
// 2. Move every client to the new name
// 3. Remove the old one
Slower and it never breaks anything.
Adding a required field — make it optional with a default first:
@IsOptional()
@IsString()
deliverySlot?: string = "ANY";
Update the clients, then require it.
Removing an endpoint — deprecate first:
@Get("old-endpoint")
@Header("Deprecation", "true")
@Header("Sunset", "Wed, 31 Dec 2026 23:59:59 GMT")
oldEndpoint() { ... }
Log every call. When the log is empty for a month, delete it.
This is the same three-step pattern as a database column rename from module 6, and for the same reason: never have a moment where both sides must change simultaneously.
Shared types as the real protection
For this project, the strongest guarantee is not versioning:
// packages/shared/src/product.ts
export interface ProductSummary {
id: string;
slug: string;
name: string;
pricePaise: number;
}
Both halves import it. Rename pricePaise and the front end fails to
compile — you find out in seconds, in your editor, rather than from a
customer.
That is module 9's subject and it is worth more day to day than any versioning scheme.
Deprecation people notice
@ApiProperty({ deprecated: true, description: "Use pricePaise instead." })
price?: number;
Three things make a deprecation work: mark it in the documentation, log when it is used so you know whether anyone still is, and give a date. A deprecation with no sunset date is a field you will still be maintaining in five years.
Check your work
Three safe changes: a new endpoint, a new optional request field, a new response field.
Three breaking ones: removing a response field, adding a required request field, changing a field's type.
Why changing meaning without renaming is the worst: nothing fails, so every client keeps working and every result is wrong.
Why adding an enum value can break clients: any client switching on it falls through to its default branch.
When versioning is genuinely needed: when clients exist that you cannot update — installed mobile apps, third parties, API customers.
Why not to version this project: the only client deploys with the API, so both can change in one commit.
The three-step rename: add the new name alongside the old, migrate clients, then remove the old one.
What protects you day to day: shared types, which turn a breaking change into a compile error.
Practice
- Sort ten changes to your API into safe and breaking. Justify the three you found hardest.
- Enable versioning with a default of
v1. Confirm/api/v1/productsworks. - Add a
v2of one endpoint with a different response shape. Confirm both run. - Rename a field in
packages/sharedand watch the front end fail to compile. Note how long it took to find out. - Do the same rename using the three-step transition. Confirm nothing breaks at any point.
- Add a required field to a DTO and confirm the existing client breaks. Redo it as optional with a default.
- Add
DeprecationandSunsetheaders to an endpoint and log its use. - Change
totalto exclude delivery without renaming it. Write down how you would ever have discovered this in production. - Decide whether the Kirana Store needs versioning and write one paragraph defending your answer.
That is module seven. Your API has a designed surface, validated input, actionable errors, a consistent list contract, and a plan for changing it.
Next module: authentication.
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