Identify and avoid Salesforce anti-patterns including SOQL N+1, governor limit violations, and API waste.
Use when reviewing Salesforce code for issues, onboarding new developers,
or auditing existing Salesforce integrations for best practices violations.
Trigger with phrases like "salesforce mistakes", "salesforce anti-patterns",
"salesforce pitfalls", "salesforce what not to do", "salesforce code review".
Instrucciones de origen · Vista previa de solo lectura
name
salesforce-known-pitfalls
description
Identify and avoid Salesforce anti-patterns including SOQL N+1, governor limit violations, and API waste.
Use when reviewing Salesforce code for issues, onboarding new developers,
or auditing existing Salesforce integrations for best practices violations.
Trigger with phrases like "salesforce mistakes", "salesforce anti-patterns",
"salesforce pitfalls", "salesforce what not to do", "salesforce code review".
allowed-tools
Read, Grep
version
1.7.0
license
MIT
author
Jeremy Longshore <jeremy@intentsolutions.io>
tags
["saas","crm","salesforce"]
compatibility
Designed for Claude Code
Salesforce Known Pitfalls
Overview
The 10 most common and costly mistakes when integrating with Salesforce, with real error messages and correct patterns.
Pitfall #1: SOQL N+1 Query Pattern (Most Common)
Anti-Pattern
// Query accounts, then query contacts for each = N+1 API callsconst accounts = await conn.query('SELECT Id, Name FROM Account LIMIT 100');
for (const account of accounts.records) {
// 100 extra API calls! (plus 100 extra SOQL queries in Apex)const contacts = await conn.query(
`SELECT Id, Name, Email FROM Contact WHERE AccountId = '${account.Id}'`
);
}
// Total: 101 API calls for what should be 1
Correct Pattern
// Single relationship query — 1 API callconst accounts = await conn.query(`
SELECT Id, Name,
(SELECT Id, FirstName, LastName, Email FROM Contacts)
FROM Account
LIMIT 100
`);
// accounts.records[0].Contacts.records → child contacts
Pitfall #2: Ignoring API Limits (Org-Wide Shared Pool)
Anti-Pattern
// This integration uses 80,000 API calls/day// Sales team uses 60,000/day// Total: 140,000 > 150,000 limit → everyone gets blocked
// 2. Use sObject Collections (1 call = 200 records)
await
sobject
'Contact'
create
// batch of up to 200
// 3. Use Bulk API for 10K+ (separate limit pool)
await
bulk2
loadAndWaitForResults
object
'Contact'
operation
'insert'
input
Pitfall #3: SOQL Injection
Anti-Pattern
// User input directly in SOQL — injectableconst name = req.query.name; // Could be: "'; SELECT Id FROM User; --"await conn.query(`SELECT Id FROM Account WHERE Name = '${name}'`);
Correct Pattern
functionescapeSoql(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
await conn.query(`SELECT Id FROM Account WHERE Name = '${escapeSoql(name)}'`);
// Fetches ALL fields — 200+ columns on Account, massive payloadconst result = await conn.query('SELECT FIELDS(ALL) FROM Account LIMIT 100');
Correct Pattern
// Select only what you need — 5-10x faster, much less data transferconst result = await conn.query('SELECT Id, Name, Industry, AnnualRevenue FROM Account LIMIT 100');
Pitfall #5: Hardcoded Salesforce Record IDs
Anti-Pattern
// IDs are different across sandbox and production!const adminProfileId = '00e5f000001abc'; // Works in sandbox...const queueId = '00G5f000002def'; // ...breaks in productionawait conn.sobject('Case').create({ OwnerId: queueId, ProfileId: adminProfileId });
Correct Pattern
// Look up by name, not by IDconst queue = await conn.query("SELECT Id FROM Group WHERE Name = 'Support Queue' AND Type = 'Queue'");
const queueId = queue.records[0].Id;
const profile = await conn.query("SELECT Id FROM Profile WHERE Name = 'System Administrator'");
const profileId = profile.records[0].Id;
Pitfall #6: Not Handling Partial Success in Bulk Operations
Anti-Pattern
const results = await conn.sobject('Contact').create(contacts);
console.log('Done!'); // Ignoring that some records may have failed
Correct Pattern
const results = await conn.sobject('Contact').create(contacts);
const failures = results.filter(r => !r.success);
if (failures.length > 0) {
console.error(`${failures.length}/${results.length} records failed:`);
for (const failure of failures) {
console.error(` ${failure.errors.map(e => `${e.statusCode}: ${e.message}`).join('; ')}`);
}
}
Pitfall #7: Using test.salesforce.com for Production
Anti-Pattern
// Sandbox login URL used for production — silently connects to sandboxconst conn = new jsforce.Connection({
loginUrl: 'https://test.salesforce.com', // WRONG for production
});
Correct Pattern
const conn = new jsforce.Connection({
loginUrl: process.env.SF_LOGIN_URL, // 'https://login.salesforce.com' for prod// OR: 'https://test.salesforce.com' for sandboxes
});
// Always use environment variables — never hardcode login URLs
Pitfall #8: Not Using External IDs for Upsert
Anti-Pattern
// Without External ID: create duplicates on every sync runawait conn.sobject('Account').create({ Name: 'Acme Corp', Industry: 'Tech' });
// Run again → duplicate Account created!
Correct Pattern
// With External ID: upsert is idempotent — safe to retryawait conn.sobject('Account').upsert({
External_ID__c: 'CRM-ACME-001', // Custom External ID fieldName: 'Acme Corp',
Industry: 'Tech',
}, 'External_ID__c');
// Run again → updates existing record, no duplicate
Pitfall #9: Missing Error Handling for UNABLE_TO_LOCK_ROW
Anti-Pattern
// Record locking is COMMON in Salesforce — this crashes on contentionawait conn.sobject('Account').update({ Id: accountId, Name: 'New Name' });
// Error: UNABLE_TO_LOCK_ROW → unhandled, crashes the process
// Poll every 5 minutes — wastes 288 API calls/day even when nothing changed
cron.schedule('*/5 * * * *', async () => {
const changes = await conn.query(`
SELECT Id, Name FROM Account WHERE LastModifiedDate >= ${fiveMinAgo}
`);
// Usually returns 0 records — wasted API call
});
Correct Pattern
// CDC — only fires when data actually changes, zero wasted API calls
conn.streaming.topic('/data/AccountChangeEvent').subscribe((event) => {
// Only called when an Account is actually created/updated/deletedhandleAccountChange(event);
});