LWC Zero to Hero Module 5 — @api Properties & Public Methods
🔌 LWC Zero to Hero — Module 5 of 15
@api Properties & Public Methods
Everything inside a component is private by default. @api is the ONLY way to open a door for parent components to pass data in and call methods. This module is also the final piece of Phase 2 — LWC Fundamentals.
Module 5 of 15 (counting Module 0 as a bonus prerequisite) · Phase 2: LWC Fundamentals (Final Module)
🎯 What You'll Master in This Module
By default, every property and method in your LWC class is PRIVATE — invisible and unreachable from outside the component.
@api is the decorator that deliberately punches a hole through that wall, exposing specific properties/methods to whatever component uses yours. Getting this right is the difference between a reusable, well-designed component and a fragile one that breaks the moment someone uses it slightly differently than you expected.✓ What @api actually does, and why everything is private without it
✓ Setting @api properties from a parent's HTML — including the camelCase-to-kebab-case rule
✓ Exposing @api methods that a parent can call directly
✓ Setter/getter pairs for validating or transforming incoming @api values
✓ Providing sensible default values for optional @api properties
✓ Reacting to @api property changes after the initial render
✓ Designing a clean, minimal component API that won't break when reused
📋 In This Module
Concept 1 of 7
What @api Actually Does
Every property and method in an LWC class is private by default — nothing outside the component can read, set, or call them. The
@api decorator marks a SPECIFIC property or method as PUBLIC, meaning a parent component (or an admin in App Builder, for @api properties) can set values into it or call it directly.⚡ Why This Matters
This is the foundational mechanism for building genuinely REUSABLE components. A generic
c-order-card component becomes useful for displaying ANY order only because it exposes @api orderId — without that, the component would only ever work with one hardcoded order.PRIVATE BY DEFAULT vs PUBLIC WITH @api
import { LightningElement, api } from 'lwc';
export default class OrderCard extends LightningElement {
@api orderId; // PUBLIC — parent can set this
internalCache = {}; // PRIVATE — invisible outside this component
@api refresh() { // PUBLIC method — parent can call this
this.loadOrderData();
}
loadOrderData() { // PRIVATE method — only usable internally
// ...
}
}
🛠️ Practical Mini-Implementation
Think of
@api as your component's PUBLIC CONTRACT — exactly like a function's parameters define what it accepts. Everything marked @api is a promise to anyone using your component: "you can rely on setting/calling this." Everything NOT marked @api is an implementation detail you're free to change later without breaking anyone.⚠️ Common Gotcha
Recall Module 2's lifecycle gotcha:
@api property values are NOT yet set when constructor() runs — the parent sets them slightly afterward, before connectedCallback() fires. Reading an @api property in constructor() reliably gives you undefined. Always read/use @api values starting from connectedCallback() onward.Concept 2 of 7
Setting @api Properties from a Parent
A parent component sets a child's
@api property by writing it as an HTML attribute on the child's custom tag. There's one critical naming rule: JavaScript property names are camelCase, but HTML attributes are written in kebab-case (dash-separated) — LWC automatically converts between the two.⚡ Why This Matters
This camelCase-to-kebab-case conversion catches almost every beginner at least once — using the wrong casing in the wrong place is one of the most common "why isn't my property being set" bugs.
CHILD COMPONENT defines the @api property (camelCase)
<!-- orderCard.js -->
export default class OrderCard extends LightningElement {
@api orderId; // camelCase in JavaScript
@api isHighPriority; // camelCase in JavaScript
}
PARENT COMPONENT sets it via kebab-case HTML attribute
<!-- orderList.html -->
<template>
<c-order-card order-id="001xx000003DGb2"
is-high-priority></c-order-card>
</template>
// order-id (kebab-case attribute) → this.orderId (camelCase property)
// is-high-priority (kebab-case) → this.isHighPriority (camelCase)
SETTING DYNAMICALLY — binding to the parent's own data
<!-- orderList.html — order-id set dynamically per loop item -->
<template for:each={orders} for:item="order">
<c-order-card key={order.Id}
order-id={order.Id}></c-order-card>
</template>
// {order.Id} here is a normal expression — dynamically passes each order's Id
🛠️ Practical Mini-Implementation
Notice the Boolean attribute pattern:
is-high-priority with NO value at all (just the attribute name present) sets the property to true. This is standard HTML boolean attribute behavior — if you need it explicitly false, simply OMIT the attribute entirely rather than writing is-high-priority="false" (which would actually still evaluate as a truthy string in some contexts — omission is the safe, unambiguous way to set a boolean @api property to false from markup).⚠️ Common Gotcha
Writing the JS property in camelCase as an HTML attribute directly (
orderId="001..." instead of order-id="001...") silently fails — HTML attributes are case-insensitive and don't support camelCase the way JavaScript does. LWC's compiler expects kebab-case in markup specifically so it can reliably convert it back to camelCase internally.Concept 3 of 7
@api Methods — Calling Into a Child
Beyond properties,
@api can also expose METHODS — letting a parent component directly trigger behavior inside a child, rather than only passing data down. To call an exposed method, the parent first needs a reference to the child element, typically obtained via this.template.querySelector().⚡ Why This Matters
Imperative actions like "reset this form," "open this modal," or "refresh this child's data on demand" can't be expressed as a simple property value — they need an actual function call, which is exactly what
@api methods provide.CHILD COMPONENT exposes a public method
<!-- searchBox.js -->
export default class SearchBox extends LightningElement {
searchTerm = '';
@api clear() { // exposed public method
this.searchTerm = '';
const input = this.template.querySelector('input');
if (input) input.value = '';
}
}
PARENT COMPONENT calls the method via querySelector reference
<!-- searchPage.html -->
<template>
<c-search-box></c-search-box>
<button onclick={handleClearClick}>Clear Search</button>
</template>
// searchPage.js
handleClearClick() {
const searchBox = this.template.querySelector('c-search-box');
if (searchBox) {
searchBox.clear(); // calling the EXPOSED @api method directly
}
}
🛠️ Practical Mini-Implementation
Always check that
querySelector actually found the element before calling a method on it (the if (searchBox) check above) — if the child component isn't rendered yet (perhaps it's inside an lwc:if block that's currently false), querySelector returns null, and calling a method on null throws a runtime error.⚠️ Common Gotcha
Relying heavily on
@api methods called imperatively from a parent is sometimes a sign you should reconsider the design — overusing this pattern can make components tightly coupled and harder to reuse independently. Many cases that seem to need an imperative method (like "reset when this prop changes") can instead be handled reactively via the setter pattern covered in Concept 4 and Concept 6.Concept 4 of 7
Setter/Getter Pairs for @api Properties
By default, an
@api property just stores whatever value the parent sets, with no validation or transformation. To intercept the incoming value — validate it, transform it, or trigger side effects when it changes — you write a SETTER and GETTER pair using the same property name, with a separate private "backing field" to actually hold the value.⚡ Why This Matters
Real-world @api properties often need validation ("this should never be negative"), normalization ("always store this trimmed and lowercase"), or need to trigger other logic the moment a new value arrives — none of which a plain @api property can do alone.
THE SETTER/GETTER PATTERN — with a private backing field
export default class OrderCard extends LightningElement {
_orderAmount = 0; // private backing field (underscore prefix is convention)
@api
get orderAmount() {
return this._orderAmount;
}
set orderAmount(value) {
// validation: never allow negative amounts
this._orderAmount = value < 0 ? 0 : value;
}
}
// @api goes ONLY on the getter — the setter is automatically paired with it
// Parent setting order-amount="-50" results in this.orderAmount being 0, not -50
TRANSFORMING incoming values
export default class SearchBox extends LightningElement {
_searchTerm = '';
@api
get searchTerm() {
return this._searchTerm;
}
set searchTerm(value) {
// normalize: always store trimmed and lowercase
this._searchTerm = (value ?? '').trim().toLowerCase();
}
}
🛠️ Practical Mini-Implementation
The underscore-prefixed backing field (
_orderAmount) is a widely-used naming CONVENTION, not a language requirement — it just makes it visually obvious which field is the "real" private storage versus the public-facing getter/setter name. Keep this convention consistent across your components for readability.⚠️ Common Gotcha
The
@api decorator goes ONLY on the getter, never on both the getter and setter, and never on the setter alone. Writing @api set orderAmount(value) {...} without also defining the paired getter (or putting @api in the wrong place) causes a compile-time error — LWC requires the getter/setter pair to be defined together with the decorator on the getter.Concept 5 of 7
Default Values for Optional @api Properties
Not every @api property should be REQUIRED — some are genuinely optional, and your component should behave sensibly even if the parent never sets them. Providing a default value prevents
undefined from silently breaking your template or logic.⚡ Why This Matters
A component that crashes or displays "undefined" on screen the moment someone forgets to set an optional property is a poorly designed, fragile component. Defaults make your component genuinely reusable by people who don't know every detail of its internals.
SIMPLE DEFAULT — assign directly on the field
export default class OrderCard extends LightningElement {
@api pageSize = 10; // default of 10 if parent doesn't set it
@api theme = 'light'; // default theme if parent doesn't set it
}
// If parent never sets page-size, this.pageSize is 10, not undefined
// If parent DOES set page-size="25", it correctly overrides the default
DEFAULT VALUES WITH SETTER/GETTER PATTERN
export default class OrderCard extends LightningElement {
_pageSize = 10; // default lives on the backing field
@api
get pageSize() {
return this._pageSize;
}
set pageSize(value) {
// guard against the parent explicitly passing undefined/null
this._pageSize = value ?? 10;
}
}
// The ?? (nullish coalescing) operator: use "value" unless it's null/undefined,
// in which case fall back to 10
🛠️ Practical Mini-Implementation
The
?? (nullish coalescing) operator shown above is the modern, precise way to apply defaults — unlike || (logical OR), ?? only falls back when the value is genuinely null or undefined, not for OTHER falsy values like 0 or empty string ''. This matters: pageSize = 0 is a deliberately set valid value, and value || 10 would incorrectly override it to 10, while value ?? 10 correctly preserves the explicit 0.⚠️ Common Gotcha
Using
|| instead of ?? for defaults is a subtle, hard-to-spot bug — it silently breaks any property where a legitimate value happens to be falsy (0, empty string, false). Always reach for ?? when writing default-value logic in modern LWC code.Concept 6 of 7
Reacting to @api Property Changes
Sometimes you need to do MORE than just store an incoming @api value — you need to trigger other logic (like fetching new data) whenever the parent updates it, even AFTER the component's initial render. The setter pattern from Concept 4 is exactly the tool for this, since the setter runs every time the parent assigns a new value, not just once.
⚡ Why This Matters
A common real scenario: a child component receiving a different
recordId later (the parent navigated to a different record) needs to re-fetch data for the NEW id — this can't be handled in connectedCallback() alone, since that only fires once.RE-FETCHING DATA WHEN AN @api PROPERTY CHANGES
export default class OrderDetail extends LightningElement {
_recordId;
@api
get recordId() {
return this._recordId;
}
set recordId(value) {
this._recordId = value;
if (value) {
this.fetchOrderData(value); // runs EVERY time a new value is set
}
}
async fetchOrderData(id) {
this.orderData = await getOrderById({ orderId: id });
}
}
// Parent changes record-id later → setter fires again → fresh data loads
// This would NOT happen if fetching only occurred in connectedCallback()
🛠️ Practical Mini-Implementation
This setter-triggers-refetch pattern is the standard solution to "my child component shows stale data when the parent navigates to a new record." It's a direct, practical application of everything from Module 4 (reactivity) and this module's setter pattern working together.
⚠️ Common Gotcha
The setter ALSO runs once during the very first render (when the parent sets the initial value), not only on subsequent changes — so you don't need separate logic in
connectedCallback() AND the setter for the same fetch; the setter alone handles both the initial load and all future updates, as long as you guard against the value being empty/undefined on that first call if needed.Concept 7 of 7
Designing Clean Component APIs
Now that you know HOW to expose properties and methods, let's talk about WHEN and HOW MANY. A component's "API" (its full set of @api properties and methods) is a design surface — and like any API, fewer well-chosen, well-named options beat a sprawling list of options nobody fully understands.
⚡ Why This Matters
Component API design directly determines whether your component gets reused happily across "XYZ Company"'s entire org, or whether everyone just copy-pastes and modifies it because the original felt too complicated or too rigid to configure properly.
❌ TOO MANY OPTIONS — overwhelming, hard to use correctly
<c-order-card
order-id={id}
show-header
show-footer
show-amount
show-status
header-color="blue"
footer-align="right"
compact-mode
disable-animation>
</c-order-card>
// 9 separate @api properties just to configure ONE card — too many decisions
✅ BETTER — fewer properties, smarter defaults, slots for true flexibility
<c-order-card order-id={id} variant="compact">
<div slot="footer">
<lightning-button label="View Details"></lightning-button>
</div>
</c-order-card>
// 2 @api properties (order-id, variant) + a slot (Module 3) for footer content
// "variant" is one well-designed property covering several visual presets at once
🛠️ Practical Mini-Implementation
A useful design heuristic before adding a new @api property: ask "could this instead be a slot?" If you're exposing a boolean to toggle whether some chunk of content shows or not, a slot (which the parent simply doesn't fill if they don't want that content) is often more flexible than a boolean flag plus hardcoded content inside your component.
⚠️ Common Gotcha — Phase 2 Wrap-Up
This module completes Phase 2 (LWC Fundamentals) — Modules 2 through 5 together (anatomy/lifecycle, templates/directives, reactivity, and now @api) form the core toolkit every single LWC component uses. If any of those four modules still feel shaky, this is the natural checkpoint to revisit them before Phase 3 introduces component-to-component communication patterns that build directly on top of everything here.
💬 Module 5 Interview Questions (6)
Q1What is the purpose of the @api decorator, and what is true about a component's properties and methods without it?
The @api decorator exposes a specific property or method publicly, allowing a parent component to set values into it or call it directly. Without @api, every property and method in an LWC class is private by default — completely invisible and unreachable from outside that specific component instance. This default-private design means a component's public API (what @api exposes) is a deliberate, explicit contract, while everything else remains a private implementation detail that can be freely changed later without affecting any component that uses it.
"@api exposes specific properties or methods as part of a component's public contract; everything else is private by default and inaccessible from outside, which keeps implementation details safely changeable without breaking consumers."
Q2A child component has @api orderId, but a parent sets it as <c-order-card orderId="001..."> and it doesn't work. What's wrong?
The HTML attribute is written in camelCase (orderId) but HTML attributes must use kebab-case (dash-separated, lowercase) — it should be written as order-id instead. LWC's compiler automatically converts between kebab-case HTML attributes and camelCase JavaScript property names, but this conversion only works when the markup actually uses kebab-case; HTML attributes are case-insensitive and don't support camelCase the way JavaScript identifiers do, so writing orderId directly in the markup silently fails to set the property correctly.
"HTML attributes must use kebab-case (order-id) even though the corresponding JavaScript @api property is camelCase (orderId) — LWC converts between the two automatically, but only when the markup itself uses proper kebab-case."
Q3Why would you use a setter/getter pair instead of a plain @api property, and where does the @api decorator go in that pattern?
A setter/getter pair is used when you need to intercept an incoming @api value to validate it, transform or normalize it, or trigger additional logic whenever a new value is set — a plain @api property simply stores whatever value it receives with no interception possible. In this pattern, a private backing field (conventionally prefixed with an underscore) holds the actual value, the getter returns that backing field's value, and the setter contains the validation/transformation logic before assigning to the backing field. The @api decorator is placed only on the getter; it should never be placed on the setter alone or on both, as LWC requires the decorator on the getter with the setter automatically paired to it.
"Setter/getter pairs allow validation, transformation, or side effects when an @api value is set, using a private backing field for actual storage; the @api decorator goes only on the getter, with the setter automatically paired to it."
Q4Why is the ?? (nullish coalescing) operator preferred over || (logical OR) when providing a default value for an @api property?
The || operator falls back to the default value for ANY falsy value, including 0, empty string, and false — not just null or undefined. This becomes a bug when a legitimately valid value happens to be falsy: for example, if pageSize is deliberately set to 0, writing value || 10 would incorrectly override it to 10, since 0 is falsy. The ?? operator only falls back to the default when the value is specifically null or undefined, correctly preserving other falsy-but-valid values like 0, false, or an empty string. This makes ?? the safer, more precise choice for default-value logic involving @api properties that might legitimately hold falsy values.
"?? only applies the default for null or undefined, while || incorrectly applies it for any falsy value including a deliberately set 0 or false, making ?? the safer choice for default values on @api properties."
Q5A parent component navigates to show a different record, updating a child's recordId @api property. The child's data doesn't refresh to match the new record. How would you fix this using the setter pattern?
The fix is to convert the plain @api recordId property into a setter/getter pair, where the setter not only stores the incoming value into a private backing field but also triggers a data-fetching method using that new value, every time the setter runs. Since the setter executes every single time the parent assigns a new value to the property — not just once during the initial render — this ensures that whenever the parent navigates to a different record and updates recordId, the setter automatically re-fetches data for the new id. Relying solely on connectedCallback() would not work for this case, since connectedCallback() only fires once when the component is first inserted into the DOM, not on subsequent property updates.
"Convert the @api property to a setter/getter pair where the setter triggers a re-fetch using the new value, since the setter runs on every property update (not just once like connectedCallback), correctly refreshing data whenever the parent changes the recordId."
Q6You're designing a reusable card component and considering adding a 6th boolean @api property to control whether a certain section displays. What design alternative should you consider, and why?
Before adding another boolean flag, it's worth considering whether a slot would be a better design choice instead. A boolean property paired with hardcoded internal content requires the component itself to define what shows when the flag is true, limiting flexibility to exactly that one predefined option. A slot, by contrast, lets the parent component supply whatever content it wants for that section, and simply omitting content for that slot achieves the same effect as a false boolean would, but with far more flexibility for different use cases. Generally, a component's API design benefits from having fewer, well-chosen properties combined with slots for genuine content flexibility, rather than an ever-growing list of boolean toggles that becomes confusing and hard to reason about for anyone reusing the component.
"Consider using a slot instead of another boolean flag — slots let the parent supply or omit content flexibly, achieving the same show/hide effect as a boolean while avoiding an ever-growing, confusing list of configuration properties."
📝 Module 5 Recap — Public Component APIs Mastered
✅ Everything is private by default; @api deliberately exposes specific properties/methods as a public contract
✅ Parents set @api properties via kebab-case HTML attributes that map to camelCase JS property names
✅ @api methods let a parent imperatively call into a child via this.template.querySelector() references
✅ Setter/getter pairs (with a private backing field, @api on the getter only) enable validation and transformation
✅ Use ?? (not ||) for default values, to correctly preserve legitimate falsy values like 0 or false
✅ A setter runs on every property update (not just once), making it the right tool for "refetch when this changes"
✅ Good component API design favors fewer well-chosen properties plus slots over a sprawling list of boolean flags
🎉 Phase 2 Complete — LWC Fundamentals Mastered!
Modules 2 through 5 gave you the complete core toolkit: component anatomy and lifecycle, templates and directives, reactivity, and public component APIs. Every component you build from here uses all four of these foundations together. Phase 3 starts next — Component Communication — where you'll connect multiple components together using events, Lightning Message Service, navigation, and more.
🎯 Before Moving to Module 6...
Try this: build a simple two-component pair — a generic c-rating-stars component exposing @api value (the current rating, 1-5) and @api readOnly (boolean, defaults to false via ??), used inside a parent component that renders three different rating-stars instances with different initial values. This single exercise touches default values, kebab-case attribute binding, and reusable component design all at once. Module 6 begins Phase 3 — Parent-Child Communication — teaching you how a CHILD can send data back UP to a parent using custom events, completing the two-way communication picture.
☕
☕ Enjoyed this article?
SF Interview Pro is 100% free and maintained by a Salesforce professional. No ads, no paywalls, and no signup required. If this guide helped you prepare for an interview, earn a certification, or grow your Salesforce career, consider buying me a coffee! ☕💜
rajnishrk264@ybl
Scan with GPay, PhonePe, Paytm, or BHIM
📚 Keep Preparing
New interview questions every week 🚀
Follow for fresh Salesforce Q&A, free courses, and real interview experiences — straight from the trenches.
👥 Follow on LinkedIn