Skip to content

SOQL and Salesforce Lightning

Lightning component requests crossing an Apex data contract to retrieve a compact SOQL result

Lightning makes it easy to get a record on screen quickly. Where things get complicated is when that record view becomes a filtered list of open cases, a metrics panel rolling up pipeline by region, or a cross-object search that needs results shaped for a specific UI. Record-based APIs like getRecord handle the simple cases well, but they are not query engines. When a component needs selective filtering, aggregation, or joined data, those patterns break down quickly.

In Lightning development, using SOQL in Apex fills that gap. It lets you write precise queries that return exactly the rows and fields a component needs, apply WHERE conditions across related objects, and aggregate metrics before results reach the component. I reach for this pattern whenever a Lightning component’s data requirements go beyond what a single record lookup can express cleanly, especially when building filtered case lists, pipeline dashboards, or custom search interfaces.

In this guide, you will learn when to use record-based access versus Apex-backed SOQL, how to wire query results to a Lightning Web Component efficiently, how to stay within governor limits under realistic load, and how to enforce field-level security correctly in current Salesforce API versions. If you want a quick primer before this Lightning-focused guide, start with integrating SOQL with Apex.

⚡ Understanding SOQL in Salesforce Lightning

Section titled “⚡ Understanding SOQL in Salesforce Lightning”

For record-centric access in LWC, use methods like getRecord and getFieldValue from lightning/uiRecordApi. These are record-based data access patterns, not query engines. When you need query-based retrieval logic (cross-object filters, selective conditions, aggregation, or custom row shaping), run SOQL in Apex and return the results to the component. Because LWC runs client-side and has no direct access to the database layer, SOQL must execute in Apex, which acts as the enforced server-side boundary for data access and security.

getRecord / uiRecordApiApex + SOQL
ScopeSingle record by IdMultiple records, filtered sets
FilteringNot supportedFull WHERE, LIMIT, ORDER BY
AggregationNot supportedCOUNT, SUM, AVG, GROUP BY
Security enforcementPlatform-managed sharing, object access, and FLSApex-owned; declare sharing and database access mode explicitly
Cross-object queriesParent field traversal onlyFull relationship queries
Best forRecord detail views, single-record formsLists, dashboards, search interfaces, custom data shapes
LWC data-access choices: use lightning/uiRecordApi for one record, or an Aura-enabled Apex method and SOQL for a filtered or custom dataset

🧱 Using SOQL in Lightning Web Components

Section titled “🧱 Using SOQL in Lightning Web Components”

To use SOQL with LWCs, you need to create an Apex controller that executes the query and exposes the data to the component. Here’s a basic example:

Apex Controller:

public with sharing class AccountController {
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts(String industry) {
return [
SELECT Id, Name, Industry
FROM Account
WHERE Industry = :industry
WITH USER_MODE
];
}
}

LWC JavaScript:

import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountList extends LightningElement {
industry = 'Technology';
@wire(getAccounts, { industry: '$industry' }) accounts;
}

The getAccounts method accepts a typed String parameter, which is passed into the query as a bind variable using :industry. This keeps the query safe from injection and makes the method reusable across different filter values. In the LWC, the $industry prefix on the wire property tells the wire service to re-run the Apex call reactively whenever industry changes.

When you design an @AuraEnabled method for an LWC, you’re not just writing a query, you’re defining a contract between the server and the UI. Treating it as such from the start makes components more reliable and the Apex layer easier to maintain and security review.

A well-defined data contract has four characteristics:

  • Typed inputs: Accept explicit, typed parameters rather than dynamic query fragments or untyped inputs. This makes the method self-documenting and keeps untrusted values in bind variables rather than in the SOQL structure.
  • Intentional output shape: Return only the fields and records the component needs. Returning a raw List<sObject> works for simple cases, but for more complex components, a wrapper class or data transfer object (DTO) makes the contract explicit and prevents accidentally surfacing fields the UI never uses.
  • Owned security enforcement: The Apex method is the security boundary. Use with sharing on the class, WITH USER_MODE in the query, or Security.stripInaccessible() where needed. Never assume the platform or the component will handle it.
  • Stable signature: The component depends on the method’s return structure. Treat signature changes like breaking API changes. Adding fields silently can expose sensitive data; removing or renaming them breaks the component.

I use this framing when reviewing LWC data access in production orgs, especially in Health Cloud implementations where security and audit requirements are strict. Components that treat their Apex methods as contracts tend to be significantly easier to security review, refactor, and hand over to other developers, and they age much better as requirements change.

  1. Governor Limits: Design with concrete limits in mind. A synchronous Apex transaction can issue up to 100 SOQL queries and retrieve up to 50,000 total queried rows. Most asynchronous transactions can issue up to 200 queries and retrieve the same 50,000 rows; scheduled Apex uses the synchronous limits. For larger workloads, a Batch Apex job using Database.QueryLocator can return up to 50 million records, and per-transaction limits reset for each execute scope (Execution Governors and Limits).
  2. Caching: The cacheable=true attribute can be used in the @AuraEnabled annotation to enable client-side caching, reducing server calls and improving performance.
  3. Progressive Retrieval: For large datasets in interactive components, prefer pagination, selective filtering, and lazy loading so each request stays small and responsive. Use imperative Apex calls when request timing must be controlled by user actions.
  4. Avoid Over-Fetching: Only SELECT the fields the component actually renders. Returning full sObjects when the component needs a few fields is one of the most common LWC performance issues I see in orgs, especially in components that surface dozens or hundreds of records. It increases payload size, slows wire adapter rendering, and makes the Apex method harder to reason about. If the field list diverges significantly from a standard sObject, a lightweight wrapper class is usually the cleaner solution.

For deeper tuning patterns, see SOQL performance optimisation techniques.

  1. Complete response: For a fixed user-facing field set, declare with sharing on the controller and query with WITH USER_MODE. Map any permission failure to a stable UI error rather than exposing permission details.
  2. Partial response: Use Security.stripInaccessible() only when the component is designed to work without defined optional fields. Return only the sanitised records and make the partial-response behaviour part of the component contract.
  3. Deliberate elevation: If a controller needs system-mode data, keep the elevated operation narrow and prove that neither raw fields nor out-of-scope rows can reach the browser.

For API 67.0 defaults, the full class/query interaction matrix, restriction and scoping rules, and security-testing guidance, use SOQL security guidance as the canonical reference.

  • Optimise Queries: Use indexed fields in the WHERE clause to improve query performance.
  • Use Bind Variables: Enhance security and performance by using bind variables in your queries.
  • Cache Only User-Safe Data: Treat @AuraEnabled(cacheable=true) as a performance feature, not a security control. Only cache data that has already been filtered for the current user and business context.
  • Error Handling: Implement robust error handling to manage exceptions and provide meaningful feedback to users.
  • Dynamic Data Display: Use SOQL to fetch and display data dynamically in Lightning components, such as lists, tables, and charts.
  • Interactive Dashboards: Build interactive dashboards that update in real-time based on user input or data changes.
  • Custom Search Interfaces: Create custom search interfaces that allow users to filter and search data using dynamic SOQL queries. For multi-object text search requirements, pair this with SOSL basics.

The most reliable LWC implementations treat the Apex layer as a genuine data contract: the component asks for exactly what it needs, the controller enforces access correctly, and neither side assumes the other will catch edge cases. I’ve found this contract-based approach is what keeps components responsive under realistic load while still respecting governor limits and security boundaries.

Once you have the core LWC pattern solid, the same principles carry directly into API-driven workflows. The main difference is that the caller changes from a Lightning component to an external system or integration layer, and I’ve seen poorly scoped queries become much harder to isolate and fix once they’re off-platform.

The next guide, SOQL API integration, picks up directly from here. It covers how to structure SOQL queries safely when the data consumer is an external system rather than a UI component, including how the shift from an on-platform session to an off-platform integration layer changes both query scope and permission enforcement.