Building for Salesforce
This guide walks through building a small but realistic Salesforce feature in Vertex: a function that creates a new Account, tags it with a classification, and exposes the operation to a Lightning component. Along the way it pulls together every Salesforce-specific construct Vertex offers.
Before you start, make sure your project is set up per Your first Salesforce project.
Define the SObject
Section titled “Define the SObject”@SObjectpub type Account { Account( Name: String, Industry: Option<String>, Rating: Option<String>, )}Three things are happening here:
@SObjecttells the Apex codegen this type is an Account record. Every@SObjecttype also gets an implicitId: Option<Id>field.IndustryandRatingareOption<String>so they can be omitted at construction time: constructor fields of anOptiontype default toNone.pubexports the type from the module so other Vertex files can import it.
Core business logic
Section titled “Core business logic”import accounts.types
type ClassifyError { InsertFailed(reason: String) MissingName}
pub fn create_classified_account( name: String, industry: String,): Result<Id, ClassifyError> { if name.trim() == "" { Error(MissingName) } else { let acc = types.Account( Name: name, Industry: Some(industry), Rating: Some(pick_rating(industry)), ) match Database.insert(acc) { Ok(id) => Ok(id), Error(dmlErr) => Error(InsertFailed(dmlErr.message)), } }}
fn pick_rating(industry: String): String { match industry { "Technology" | "Healthcare" => "Hot", "Retail" => "Warm", _ => "Cold", }}Notes on this file:
- Error types are explicit. Callers know at compile time what can go wrong.
Database.insertreturnsResult. We translate the genericDmlErrorinto our own domain error.pick_ratingis private. Nopubmeans it is module-local.
Expose it to Lightning
Section titled “Expose it to Lightning”import accounts.classify
@AuraEnabledpub type ClassifyResult { ClassifyResult(accountId: String, success: Bool)}
@AuraEnabledpub fn classify_for_lwc( name: String, industry: String,): Result<ClassifyResult, AuraError> { match classify.create_classified_account(name: name, industry: industry) { Ok(id) => Ok(ClassifyResult(accountId: id.toString(), success: true)), Error(classify.MissingName) => Error(AuraError("Name is required")), Error(classify.InsertFailed(msg)) => Error(AuraError("Could not save: ${msg}")), }}Notes:
@AuraEnabledon the type makes every field serializable to LWC.@AuraEnabledon the function exposes it, and the return type must beResult<_, AuraError>.Okbecomes the value passed to LWC;Errorbecomes anAuraHandledExceptionwith your message.- The boundary is narrow. The rich
ClassifyErrorlives inside Vertex; the LWC sees only a single-line message.
Sharing and security
Section titled “Sharing and security”By default, every Vertex module compiles to public with sharing, the
safe Salesforce default. To opt into a different sharing mode, add an
annotation at the top of the file:
@WithoutSharing
import accounts.classify// ...You almost never want this. Use it only when you have a specific reason (a scheduled job that needs to see all records) and audit the code accordingly.
Building and deploying
Section titled “Building and deploying”From the project root:
$ vertex buildSuccess! Built 3 class(es) to force-app/main/default/classes/$ sf project deploy start --target-org <alias>sf tracks local changes on its own; do not pass --source-dir or
--metadata.
Testing before deploying
Section titled “Testing before deploying”import accounts.classifyimport accounts.types
@Testfn rejects_blank_name(): Void { let result = classify.create_classified_account(name: " ", industry: "Retail") let assert Error(classify.MissingName) = result}
@Testfn returns_id_on_success(): Void { let result = classify.create_classified_account(name: "Acme", industry: "Retail") let assert Ok(_) = result}Run:
$ vertex test✓ rejects blank name✓ returns id on success2 passed, 0 failed (8ms)The second test exercises Database.insert without a real org. In JIT
mode, the DML layer is simulated and Database.insert returns a stub
Id. The test verifies your code does the right thing; org-side
behaviour is verified with an actual deploy.
Calling from LWC
Section titled “Calling from LWC”In your Lightning component:
import classifyForLwc from '@salesforce/apex/Aura.vtx_classify_for_lwc';
classifyForLwc({ name: 'Acme', industry: 'Retail' }) .then(result => { /* { accountId, success } */ }) .catch(error => { /* AuraHandledException message */ });The Apex method name is vtx_ + the Vertex function name.
What to remember
Section titled “What to remember”- Keep domain logic in pure Vertex modules.
@SObjecttypes andDatabase.*calls are fine, but keep@AuraEnabledboundaries thin. - Error types communicate the API contract. Don’t collapse them to strings until you are exiting the Vertex world.
- Test before you deploy. The simulated DML layer covers most of your code; a quick deploy covers the rest.
- Default to
with sharing. Only change it with a reason.
See it in a real project
Section titled “See it in a real project”vertex-run/vertex-library-management
is a sample app that applies the patterns in this guide end to end:
@SObject types, Database.* through Result, @AuraEnabled
controllers, and a domain/ module with tests that run on the JIT.
Browse the source in
src/.