/** * REST API endpoint for {SObject} operations. * Exposes CRUD operations via @RestResource at /services/apexrest/{urlPath}/v1/*. * Uses versioned URL mapping for future-proof API evolution. * All queries use WITH USER_MODE for CRUD/FLS enforcement. * * Authentication: OAuth 2.0 via Connected App * Base URL: /services/apexrest/{urlPath}/v1/ */ @RestResource(urlMapping='/{urlPath}/v1/*') global with sharing class {ClassName} { // ─── Constants ──────────────────────────────────────────────────────── private static final Integer DEFAULT_PAGE_SIZE = 20; private static final Integer MAX_PAGE_SIZE = 200; private static final String ERROR_MISSING_ID = 'Record Id is required in the URL path.'; private static final String ERROR_INVALID_ID = 'Invalid Salesforce Id format.'; private static final String ERROR_MISSING_BODY = 'Request body is required.'; private static final String ERROR_NOT_FOUND = '{SObject} record not found.'; private static final String ERROR_INSUFFICIENT_ACCESS = 'Insufficient access to perform this operation.'; // ─── GET — Retrieve ─────────────────────────────────────────────────── /** * Retrieves a single {SObject} by Id (URL path) or a paginated list (query params). * Single: GET /services/apexrest/{urlPath}/v1/{recordId} * List: GET /services/apexrest/{urlPath}/v1?pageSize=20&offset=0 * @return ApiResponse containing the requested data */ @HttpGet global static ApiResponse doGet() { RestRequest req = RestContext.request; RestResponse res = RestContext.response; try { String recordId = extractIdFromUri(req.requestURI); if (String.isNotBlank(recordId)) { return getSingleRecord(recordId, res); } return getRecordList(req, res); } catch (Exception e) { return handleError(res, 500, e.getMessage()); } } // ─── POST — Create ─────────────────────────────────────────────────── /** * Creates a new {SObject} record from the JSON request body. * POST /services/apexrest/{urlPath}/v1/ * @return ApiResponse with the created record Id */ @HttpPost global static ApiResponse doPost() { RestRequest req = RestContext.request; RestResponse res = RestContext.response; try { if (req.requestBody == null || String.isBlank(req.requestBody.toString())) { return handleError(res, 400, ERROR_MISSING_BODY); } {ClassName}Request payload = ({ClassName}Request) JSON.deserialize( req.requestBody.toString(), {ClassName}Request.class ); // TODO: Map request payload to SObject fields {SObject} record = new {SObject}( Name = payload.name ); Database.SaveResult saveResult = Database.insert(record, true); if (saveResult.isSuccess()) { res.statusCode = 201; return new ApiResponse(true, 'Record created successfully.', record.Id); } return handleError(res, 422, saveResult.getErrors()[0].getMessage()); } catch (JSONException e) { return handleError(res, 400, 'Malformed JSON: ' + e.getMessage()); } catch (DmlException e) { return handleError(res, 422, 'Validation failed: ' + e.getDmlMessage(0)); } catch (Exception e) { return handleError(res, 500, e.getMessage()); } } // ─── PATCH — Partial Update ─────────────────────────────────────────── /** * Partially updates an existing {SObject} record. * PATCH /services/apexrest/{urlPath}/v1/{recordId} * @return ApiResponse confirming the update */ @HttpPatch global static ApiResponse doPatch() { RestRequest req = RestContext.request; RestResponse res = RestContext.response; try { String recordId = extractIdFromUri(req.requestURI); if (String.isBlank(recordId)) { return handleError(res, 400, ERROR_MISSING_ID); } if (!isValidSalesforceId(recordId)) { return handleError(res, 400, ERROR_INVALID_ID); } if (req.requestBody == null || String.isBlank(req.requestBody.toString())) { return handleError(res, 400, ERROR_MISSING_BODY); } List<{SObject}> existing = [ SELECT Id FROM {SObject} WHERE Id = :recordId WITH USER_MODE LIMIT 1 ]; if (existing.isEmpty()) { return handleError(res, 404, ERROR_NOT_FOUND); } Map fieldUpdates = (Map) JSON.deserializeUntyped( req.requestBody.toString() ); {SObject} record = existing[0]; // TODO: Apply allowed field updates from the payload to the record // for (String fieldName : fieldUpdates.keySet()) { // record.put(fieldName, fieldUpdates.get(fieldName)); // } Database.SaveResult saveResult = Database.update(record, true); if (saveResult.isSuccess()) { res.statusCode = 200; return new ApiResponse(true, 'Record updated successfully.', record.Id); } return handleError(res, 422, saveResult.getErrors()[0].getMessage()); } catch (JSONException e) { return handleError(res, 400, 'Malformed JSON: ' + e.getMessage()); } catch (DmlException e) { return handleError(res, 422, 'Validation failed: ' + e.getDmlMessage(0)); } catch (Exception e) { return handleError(res, 500, e.getMessage()); } } // ─── DELETE — Remove ────────────────────────────────────────────────── /** * Deletes a {SObject} record by Id. * DELETE /services/apexrest/{urlPath}/v1/{recordId} * @return ApiResponse confirming the deletion */ @HttpDelete global static ApiResponse doDelete() { RestRequest req = RestContext.request; RestResponse res = RestContext.response; try { String recordId = extractIdFromUri(req.requestURI); if (String.isBlank(recordId)) { return handleError(res, 400, ERROR_MISSING_ID); } if (!isValidSalesforceId(recordId)) { return handleError(res, 400, ERROR_INVALID_ID); } List<{SObject}> existing = [ SELECT Id FROM {SObject} WHERE Id = :recordId WITH USER_MODE LIMIT 1 ]; if (existing.isEmpty()) { return handleError(res, 404, ERROR_NOT_FOUND); } Database.DeleteResult deleteResult = Database.delete(existing[0], true); if (deleteResult.isSuccess()) { res.statusCode = 200; return new ApiResponse(true, 'Record deleted successfully.', recordId); } return handleError(res, 422, deleteResult.getErrors()[0].getMessage()); } catch (DmlException e) { return handleError(res, 422, e.getDmlMessage(0)); } catch (Exception e) { return handleError(res, 500, e.getMessage()); } } // ─── Private Helpers ────────────────────────────────────────────────── private static ApiResponse getSingleRecord(String recordId, RestResponse res) { if (!isValidSalesforceId(recordId)) { return handleError(res, 400, ERROR_INVALID_ID); } List<{SObject}> records = [ SELECT Id, Name, CreatedDate, LastModifiedDate FROM {SObject} WHERE Id = :recordId WITH USER_MODE LIMIT 1 ]; if (records.isEmpty()) { return handleError(res, 404, ERROR_NOT_FOUND); } res.statusCode = 200; ApiResponse response = new ApiResponse(true, 'Record retrieved successfully.', recordId); response.data = records[0]; return response; } private static ApiResponse getRecordList(RestRequest req, RestResponse res) { Integer pageSize = getIntParam(req, 'pageSize', DEFAULT_PAGE_SIZE); Integer offset = getIntParam(req, 'offset', 0); pageSize = Math.min(pageSize, MAX_PAGE_SIZE); List<{SObject}> records = [ SELECT Id, Name, CreatedDate, LastModifiedDate FROM {SObject} WITH USER_MODE ORDER BY Name ASC LIMIT :pageSize OFFSET :offset ]; res.statusCode = 200; ApiResponse response = new ApiResponse(true, 'Records retrieved successfully.', null); response.records = records; response.pageSize = pageSize; response.offset = offset; return response; } private static String extractIdFromUri(String uri) { String lastSegment = uri.substringAfterLast('/'); if (String.isBlank(lastSegment) || lastSegment == 'v1') { return null; } return lastSegment; } private static Boolean isValidSalesforceId(String idValue) { return Pattern.matches('[a-zA-Z0-9]{15,18}', idValue); } private static Integer getIntParam(RestRequest req, String paramName, Integer defaultValue) { String paramValue = req.params.get(paramName); if (String.isBlank(paramValue)) { return defaultValue; } try { return Integer.valueOf(paramValue); } catch (TypeException e) { return defaultValue; } } private static ApiResponse handleError(RestResponse res, Integer statusCode, String message) { res.statusCode = statusCode; return new ApiResponse(false, message, null); } // ─── Request / Response DTOs ────────────────────────────────────────── /** * Inbound request payload for POST operations. * Extend with additional fields as needed. */ global class {ClassName}Request { global String name; // TODO: Add fields matching the expected JSON request body } /** * Standardized API response envelope. * All endpoints return this structure for consistent client parsing. */ global class ApiResponse { global Boolean success; global String message; global String recordId; global SObject data; global List records; global Integer pageSize; global Integer offset; global ApiResponse(Boolean success, String message, String recordId) { this.success = success; this.message = message; this.recordId = recordId; } } }