The Service Layer: Queries, Logic, and Error Handling
Series: Build a Salesforce Guide App with AI
Page: 4 of 10
Time to complete: 30–45 minutes
Prerequisites: Pages 2 and 3 complete
How This Page Works
This page has a different structure from the Setup pages. Instead of click-by-click instructions, it shows you:
- The prompt to give Claude
- What to do with what Claude produces
- The errors you might hit and exactly how to fix them
This is the AI-assisted development workflow in practice.
The Context-Setting Prompt
Before asking Claude to write any code, give it the full context of your project. This single prompt at the start of your conversation makes every subsequent request much more accurate.
Open claude.ai and paste this:
💬 Context-setting prompt:
I’m building a Salesforce internal guide application. Here’s the context you need before writing any code:
Org type: Trailhead Developer Edition API version: Check my org’s version — I’ll tell you if needed
Custom objects already created:
Guide__c— stores guide content. Key fields: Name, Summary__c, Body__c (Rich Text), Status__c (Draft/Pending Approval/Published/Archived), Category__c, Audience__c (User/Admin), Version__c, Tags__c, Helpful_Count__c, Is_Active__cGuide_Topic__c— topics. Fields: Name, Topic_Type__c (Object/Functionality/Subject/Admin), Object_API_Name__c, Is_Active__cGuide_Topic_Assignment__c— junction object. Master-detail to Guide__c (child relationship: Topic_Assignments__r) and Guide_Topic__cGuide_Video__c— child videos. Master-detail to Guide__c (child relationship: Guide_Videos__r). Fields: Video_URL__c (Text), Video_Type__c (YouTube/Vimeo/External Link/Salesforce File), Sort_Order__c, Description__cWhat I’m building: A single Apex class called
GuideServicewith@AuraEnabledmethods that my Lightning Web Components will call. It should usewith sharing.Security constraints:
- Viewers should only ever see Published guides with Audience = User (or null)
- Guide Admins can see all statuses
- All writes must use Security.stripInaccessible
Please confirm you understand this context before I ask for the first method.
Claude will confirm and may ask clarifying questions. Answer them, then proceed to request the methods.
Requesting the Methods One Group at a Time
Don’t ask for the entire class at once. Ask for logical groups of methods, review each, then continue. This gives you smaller pieces to verify and makes errors easier to isolate.
Group 1 — The query methods
💬 Prompt:
Write the following methods for GuideService.cls:
getGuides(String searchTerm, String category)— returns published, active, User-audience guides filtered by search term (searches Name, Summary, Tags) and category. When category is ‘All’ or blank, return all. When a specific category is given, filter by it. Mark as cacheable.
getGuideDetail(Id recordId)— returns a single Guide__c with all fields plus subqueries for Topic_Assignments__r (including Guide_Topic__r fields) and Guide_Videos__r (ordered by Sort_Order__c). Should be visible to the owner even if Draft. Mark as cacheable.
getTopics()— returns all active topics ordered by Topic_Type__c then Name. Mark as cacheable.Use
List<Guide__c>style return types. All methods should throw AuraHandledException with clear messages on failure.
⚠️ Critical: the SOQL category filter
When Claude writes the category filter, watch for this pattern:
AND (Category__c = :cat OR :cat = 'All')This is invalid SOQL. Salesforce requires bind variables to be on the right side of comparisons. The
:cat = 'All'part will fail withUnexpected token ':'.The correct approach is two separate queries using an
if/else:Boolean allCategories = String.isBlank(category) || category == 'All'; if (allCategories) { return [ SELECT ... FROM Guide__c WHERE Status__c = 'Published' ... ]; } else { return [ SELECT ... FROM Guide__c WHERE Status__c = 'Published' AND Category__c = :category ... ]; }If Claude produces the invalid pattern, say: “The SOQL bind variable on the left side of a comparison is invalid in Salesforce. Please rewrite getGuides() using two separate queries with an if/else on a boolean allCategories variable.”
Group 2 — The write methods
💬 Prompt:
Now add these write methods to GuideService.cls:
saveGuide(String guideJSON, List<Id> topicIds)— deserialises a Guide__c from JSON, upserts it using Security.stripInaccessible, then atomically replaces all topic assignments (delete existing, insert new). Returns the guide Id.
saveVideos(Id guideId, String videosJSON)— replaces all Guide_Video__c records for the guide. Each video in the JSON has url, description. Auto-detect Video_Type__c from the URL: YouTube if it contains youtube.com or youtu.be, Vimeo if it contains vimeo.com, Salesforce File if it starts with 069 and is 15-18 chars, External Link otherwise. Convert YouTube and Vimeo URLs to embed format. Note:descis a reserved word in Apex — usevideoDescas the variable name.
submitForApproval(Id guideId)— sets Status to Pending Approval via DML, then calls Approval.process(). If Approval.process() fails, roll back Status to Draft and throw AuraHandledException.
publishAdminGuide(Id guideId)— sets Status to Published directly. Should validate that Audience__c = ‘Admin’ before proceeding.
submitVote(Id guideId, Boolean isHelpful)— increments or decrements Helpful_Count__c by 1. No feedback records — fully anonymous.
⚠️ Watch for:
descas a variable nameClaude sometimes names variables
descwhen parsing description fields.descis a reserved word in Apex (used inORDER BY x DESC). If you see this, tell Claude: “The variable namedescis a reserved word in Apex. Please rename it tovideoDescthroughout the saveVideos method.”
Group 3 — The contextual query methods
💬 Prompt:
Add three more query methods:
getGuidesForModal(String objectApiName)— returns published, User-audience guides tagged with either Object topics matching the objectApiName, or any Functionality topics. Each guide should include a subquery on Topic_Assignments__r so the calling component can determine the topic type. Return empty list if objectApiName is blank.
getAdminGuides(String searchTerm)— returns published guides where Audience__c = ‘Admin’. Searches Name, Tags, Summary. Ordered by Category then Name.
getGuidesByTopic(String objectApiName)— returns published, User-audience guides tagged with topics where Object_API_Name__c matches the given value. Used by the contextual sidebar. Return empty list if objectApiName is blank or no matching topics exist.All three cacheable.
Group 4 — The test class
💬 Prompt:
Write a complete test class called
GuideServiceTestthat tests all methods in GuideService. Requirements:
- Use @TestSetup to create shared test data: one Guide_Topic__c with Topic_Type = Object and Object_API_Name = Account, three Guide__c records (one Published/Sales, one Published/Support, one Draft), one Guide_Topic_Assignment__c linking the Published Sales guide to the Account topic
- Minimum 90% coverage
- Use System.assertEquals with descriptive failure messages
- Test both happy paths and error conditions (null inputs, missing records)
- Do not use List<Guide__c> with angle brackets in the test class name or @isTest annotation — use the array notation style Guide__c[] where needed to avoid copy-paste issues
Write the full class — do not truncate.
Deploying the Code
The file encoding problem
When you copy code from Claude and paste it into VS Code files, angle brackets in generic types like List<Guide__c> may be corrupted to List<Guide__c> on Windows. This causes deployment failures with errors like Unexpected token ':'.
The safest approach is to use PowerShell to write the files directly. Ask Claude:
💬 Prompt:
Please generate a PowerShell script that writes GuideService.cls and GuideServiceTest.cls to my project. The script should:
- Base64-encode all file content so no angle brackets appear in the script itself
- Use [System.IO.File]::WriteAllBytes() to write the files (not Set-Content)
- Write to force-app\main\default\classes\
- Print a success message when done
My project is at C:\Users[username]\Desktop\guide-hub — adjust the path as needed.
Download the script, unblock it, run it:
Unblock-File -Path .\write-apex.ps1
.\write-apex.ps1
Then deploy:
sf project deploy start --source-dir force-app/main/default/classes
Reading the Error Output
Salesforce deployment errors are specific and actionable. Here’s how to read them:
ApexClass | GuideService | Unexpected token ':'. (16:40)
This means: in GuideService.cls, on line 16 at column 40, there’s an unexpected colon. In Apex, this almost always means a SOQL bind variable on the wrong side of a comparison.
ApexClass | GuideService | Invalid identifier 'public'. (1:1)
This means the file has a BOM character at the start. The file was written with Set-Content -Encoding UTF8 instead of WriteAllBytes. Re-write the file using the base64 PowerShell approach.
ApexClass | GuideServiceTest | Dependent class is invalid
The test class references GuideService which has errors. Fix GuideService first — the test class errors will resolve automatically.
When you get an error, paste the exact error table into Claude:
💬 How to report errors to Claude:
I got these deployment errors. Please identify the root cause and provide a fix:
[paste the error table exactly as it appears in the terminal]
Here is the relevant section of the code:
[paste lines around the error line numbers]
Run the Tests
Once the deploy succeeds:
sf apex run test --class-names GuideServiceTest --result-format human --wait 5
You’re looking for:
Tests Ran: 16
Passed: 16
Failed: 0
Apex Code Coverage
GuideService 92%
If tests fail, paste the failure details into Claude and ask for a fix.
Check Your Work
- ✅
sf project deploy startcompletes with no component failures - ✅ All tests pass with 90%+ coverage
- ✅ GuideService and GuideServiceTest appear in Setup → Apex Classes
- ✅ No errors in the Developer Console Problems tab
Move to Page 5 — building the Lightning Web Components.