BAPI vs RFC vs Custom Tables: Choosing the Right Integration Method
BAPI vs RFC vs Custom Tables: Choosing the Right Integration Method
When building SAP integrations, choosing between BAPI functions, RFC-enabled modules, and direct table access can make or break your project. Each approach offers distinct advantages and trade-offs that directly impact security, performance, and maintainability. In this comprehensive guide, we'll explore all three methods with real-world examples to help you make the right architectural decision.
Key Takeaway:
BAPIs provide the safest, most maintainable integration path. RFCs offer flexibility for custom logic. Direct table access should be reserved for read-only scenarios with extreme caution.
Understanding the Three Integration Approaches
SAP systems expose data and functionality through multiple channels. Before diving into code examples, let's establish a clear mental model of what each approach represents and where it fits in SAP's architecture.
BAPI (Business Application Programming Interface)
BAPIs are SAP's officially supported, business-oriented API layer. Think of them as the "front door" to SAP functionality. These RFC-enabled function modules follow strict naming conventions (typically starting with BAPI_) and are explicitly designed for external consumption. SAP guarantees their stability across releases and maintains comprehensive documentation.
When you call BAPI_SALESORDER_CREATEFROMDAT2, you're not just inserting rows into VBAK and VBAP tables. You're triggering
a complete business process that validates customer credit limits, checks
material availability, updates document flow, and maintains data consistency
across dozens of related tables.
RFC (Remote Function Call)
RFC is the underlying protocol that enables remote communication with SAP systems. While BAPIs use RFC, not all RFC-enabled function modules are BAPIs. The RFC category includes thousands of function modules that SAP uses internally—some documented, many not. These modules offer more granular control but come without SAP's stability guarantees.
RFC modules like RFC_READ_TABLE provide generic functionality that doesn't fit the business object model. They're
powerful tools but require deeper SAP knowledge to use correctly and safely.
Direct Table Access
Direct table access means connecting to SAP's underlying database (typically HANA, Oracle, or DB2) and querying tables directly via SQL. This bypasses SAP's application layer entirely. While this offers maximum flexibility and performance for read operations, it's the equivalent of picking the lock on SAP's back door—technically possible but fraught with risk.
BAPI Integration: The Recommended Standard
BAPIs represent SAP's contract with the integration community. They're the only integration method that SAP explicitly supports and maintains across system upgrades. If a BAPI exists for your use case, it should be your default choice.
When to Use BAPIs
- Creating or modifying business documents: Sales orders, purchase orders, invoices, goods movements
- Master data management: Customer, vendor, material, or employee records
- Financial transactions: Posting documents, clearing accounts, managing cost centers
- Production scenarios requiring SAP support: When you need vendor backing for your integration
- Long-term maintainability: Projects that will span multiple SAP releases
BAPI Code Example: Creating a Sales Order
Here's a practical example using Node.js with the node-rfc library to create a sales order through BAPI_SALESORDER_CREATEFROMDAT2:
import { Client } from 'node-rfc';
const sapClient = new Client({
user: process.env.SAP_USER,
passwd: process.env.SAP_PASSWORD,
ashost: process.env.SAP_HOST,
sysnr: process.env.SAP_SYSTEM_NUMBER,
client: process.env.SAP_CLIENT
});
async function createSalesOrder(orderData) {
try {
await sapClient.open();
const result = await sapClient.call('BAPI_SALESORDER_CREATEFROMDAT2', {
ORDER_HEADER_IN: {
DOC_TYPE: 'OR', // Standard order
SALES_ORG: '1000',
DISTR_CHAN: '10',
DIVISION: '00',
PURCH_NO_C: orderData.customerPO,
REQ_DATE_H: orderData.requestedDate
},
ORDER_PARTNERS: [{
PARTN_ROLE: 'AG', // Sold-to party
PARTN_NUMB: orderData.customerId
}, {
PARTN_ROLE: 'WE', // Ship-to party
PARTN_NUMB: orderData.shipToId
}],
ORDER_ITEMS_IN: orderData.items.map((item, index) => ({
ITM_NUMBER: String((index + 1) * 10).padStart(6, '0'),
MATERIAL: item.materialNumber,
TARGET_QTY: item.quantity,
PLANT: '1000'
})),
ORDER_SCHEDULES_IN: orderData.items.map((item, index) => ({
ITM_NUMBER: String((index + 1) * 10).padStart(6, '0'),
REQ_QTY: item.quantity,
REQ_DATE: orderData.requestedDate
}))
});
// Always check return messages
const errors = result.RETURN.filter(msg => msg.TYPE === 'E' || msg.TYPE === 'A');
if (errors.length > 0) {
throw new Error(`BAPI errors: ${errors.map(e => e.MESSAGE).join('; ')}`);
}
// Commit the transaction
await sapClient.call('BAPI_TRANSACTION_COMMIT', {
WAIT: 'X'
});
return {
salesOrderNumber: result.SALESDOCUMENT,
success: true
};
} catch (error) {
// Rollback on error
await sapClient.call('BAPI_TRANSACTION_ROLLBACK');
throw error;
} finally {
await sapClient.close();
}
}
// Usage
const newOrder = await createSalesOrder({
customerPO: 'CUST-PO-12345',
customerId: '1000001',
shipToId: '1000002',
requestedDate: '20250315',
items: [
{ materialNumber: 'MAT-12345', quantity: 10 },
{ materialNumber: 'MAT-67890', quantity: 5 }
]
}); BAPI Best Practices
- Always check RETURN parameters: BAPIs don't throw exceptions. They populate RETURN tables with messages typed as Error (E), Warning (W), Information (I), Abort (A), or Success (S).
- Commit your transactions: Most BAPIs don't auto-commit. Call BAPI_TRANSACTION_COMMIT for success or BAPI_TRANSACTION_ROLLBACK for failures.
- Use simulation mode when available: Many BAPIs offer TESTRUN parameters to validate without committing.
- Respect field lengths and formats: SAP is strict. A date must be YYYYMMDD, not YYYY-MM-DD. Leading zeros matter for material numbers.
- Implement idempotency: Network issues happen. Design your integration to handle duplicate calls gracefully.
BAPI Performance Considerations
BAPIs prioritize correctness over speed. A single BAPI_SALESORDER_CREATEFROMDAT2 call might execute hundreds of validation steps and touch dozens of tables. For high-volume scenarios (thousands of records), consider:
- Batch-enabled BAPIs: Some BAPIs accept arrays. BAPI_GOODSMVT_CREATE can process multiple material movements in one call.
- IDoc interfaces: For bulk data loads, IDocs offer better throughput than BAPIs.
- Connection pooling: RFC connection establishment is expensive. Reuse connections where possible.
RFC Integration: Power and Flexibility
Beyond BAPIs, SAP exposes thousands of RFC-enabled function modules. These range from widely-used utilities like RFC_READ_TABLE to obscure internal functions. RFCs give you access to SAP functionality that might not have a corresponding BAPI, but this power comes with caveats.
When to Use RFC Modules
- No suitable BAPI exists: You need functionality that SAP hasn't exposed through BAPIs
- Read-only data extraction: RFC_READ_TABLE for querying custom tables or configurations
- Custom Z-functions: Your organization has built RFC-enabled custom functions
- SAP-provided utilities: Functions like BAPI_USER_GET_DETAIL (technically a BAPI) or TH_USER_INFO
- Development/testing scenarios: Where stability guarantees matter less
RFC Code Example: Reading Table Data
RFC_READ_TABLE is probably the most controversial function module in SAP integration. It's incredibly useful but also risky if misused. Here's how to use it responsibly:
async function readSAPTable(tableName, fields, whereClause, maxRows = 1000) {
await sapClient.open();
try {
const result = await sapClient.call('RFC_READ_TABLE', {
QUERY_TABLE: tableName,
DELIMITER: '|',
FIELDS: fields.map(field => ({ FIELDNAME: field })),
OPTIONS: whereClause ? [{ TEXT: whereClause }] : [],
ROWCOUNT: maxRows,
ROWSKIPS: 0
});
// Parse the pipe-delimited response
const headers = result.FIELDS.map(f => f.FIELDNAME);
const data = result.DATA.map(row => {
const values = row.WA.split('|');
return headers.reduce((obj, header, index) => {
obj[header] = values[index]?.trim() || '';
return obj;
}, {});
});
return data;
} finally {
await sapClient.close();
}
}
// Usage: Get active sales orders
const orders = await readSAPTable(
'VBAK', // Table name
['VBELN', 'ERDAT', 'KUNNR'], // Fields to retrieve
"VKORG = '1000' AND AUART = 'OR'", // WHERE clause
500 // Max rows
);
console.log(`Retrieved ${orders.length} sales orders`);
orders.forEach(order => {
console.log(`Order: ${order.VBELN}, Customer: ${order.KUNNR}, Date: ${order.ERDAT}`);
}); RFC Security Implications
RFC_READ_TABLE is so powerful that many SAP security teams restrict it heavily or disable it entirely. Understanding why reveals critical security principles:
- Bypasses authorization objects: RFC_READ_TABLE checks S_RFC authorization but not table-level authorization objects like S_TABU_DIS. A user might read VBAK (sales orders) via RFC_READ_TABLE even if they can't access transaction VA03.
- No field-level security: If a table contains sensitive fields (salary data in PA0008), RFC_READ_TABLE returns everything unless you implement custom authorization checks.
- Performance impact: Unrestricted queries can bring SAP systems to their knees. Always specify ROWCOUNT and use selective WHERE clauses.
- No audit trail: Unlike transaction codes, RFC calls may not trigger SAP's change document mechanism.
Building Custom Z-Functions
For production integrations requiring non-BAPI functionality, building custom RFC-enabled function modules is often the best approach. This gives you BAPI-like stability with custom logic:
FUNCTION Z_GET_CUSTOMER_ORDERS.
*"----------------------------------------------------------------------
*"*"Local Interface:
*" IMPORTING
*" VALUE(IV_CUSTOMER) TYPE KUNNR
*" VALUE(IV_DATE_FROM) TYPE DATUM OPTIONAL
*" VALUE(IV_DATE_TO) TYPE DATUM OPTIONAL
*" EXPORTING
*" VALUE(EV_ORDER_COUNT) TYPE I
*" TABLES
*" ET_ORDERS STRUCTURE ZVBAK_EXT
*" ET_RETURN STRUCTURE BAPIRET2
*" EXCEPTIONS
*" NO_AUTHORITY
*"----------------------------------------------------------------------
DATA: lt_orders TYPE TABLE OF vbak,
ls_return TYPE bapiret2.
" Check authorization
AUTHORITY-CHECK OBJECT 'V_VBAK_VKO'
ID 'VKORG' DUMMY
ID 'VTWEG' DUMMY
ID 'SPART' DUMMY
ID 'ACTVT' FIELD '03'. " Display
IF sy-subrc <> 0.
RAISE NO_AUTHORITY.
ENDIF.
" Query with proper WHERE clause
SELECT * FROM vbak
INTO TABLE lt_orders
WHERE kunnr = iv_customer
AND erdat BETWEEN iv_date_from AND iv_date_to
ORDER BY erdat DESCENDING.
" Transform to external structure
LOOP AT lt_orders INTO DATA(ls_order).
APPEND INITIAL LINE TO et_orders ASSIGNING FIELD-SYMBOL(<fs_order>).
<fs_order>-vbeln = ls_order-vbeln.
<fs_order>-erdat = ls_order-erdat.
<fs_order>-netwr = ls_order-netwr.
" Additional field mappings...
ENDLOOP.
ev_order_count = lines( et_orders ).
" Success message
ls_return-type = 'S'.
ls_return-message = |Retrieved { ev_order_count } orders|.
APPEND ls_return TO et_return.
ENDFUNCTION. This approach combines the best of both worlds: custom business logic with BAPI-like error handling and authorization checks.
Direct Table Access: Use with Extreme Caution
Connecting directly to SAP's database bypasses the application layer entirely. This can be tempting for read-heavy analytics scenarios, but it's a path filled with pitfalls.
When Direct Table Access Might Be Justified
- Read-only analytics on non-production systems: Development or QA environments where data integrity isn't critical
- SAP has explicitly approved: Some SAP solutions (like BW extractors) use direct reads with SAP's blessing
- Performance-critical read scenarios: Reporting on millions of records where RFC overhead is prohibitive
- System monitoring: Reading system tables for health checks when RFC isn't available
Direct Table Access Example
import { createConnection } from '@sap/hana-client';
const conn = createConnection();
conn.connect({
host: process.env.HANA_HOST,
port: process.env.HANA_PORT,
user: process.env.HANA_USER,
password: process.env.HANA_PASSWORD,
schema: 'SAPSR3' // Your SAP schema
});
async function queryMaterialDocuments(plant, dateFrom) {
const sql = \`
SELECT
MBLNR,
MJAHR,
BUDAT,
MATNR,
MENGE,
MEINS
FROM MSEG
WHERE WERKS = ?
AND BUDAT >= ?
AND BWART IN ('101', '102', '261', '262')
ORDER BY BUDAT DESC
FETCH FIRST 1000 ROWS ONLY
\`;
const result = await conn.exec(sql, [plant, dateFrom]);
return result;
}
// Usage
const movements = await queryMaterialDocuments('1000', '20250101');
console.log(\`Found \${movements.length} material movements\`); Why Direct Access is Dangerous
- No business logic: Tables don't enforce business rules. VBAK might contain a sales order, but without application logic, you won't know if it's been deleted (marked with deletion flag), blocked, or superseded.
- Data inconsistency: Many SAP objects span multiple tables. A sales order involves VBAK (header), VBAP (items), VBEP (schedule lines), VBPA (partners), and more. Direct queries might catch data mid-transaction.
- Cluster and pool tables: Some critical SAP tables (BSEG for document line items) are stored in cluster or pool tables (BSEC, BSED, etc.) with compressed, binary formats. Direct queries are nearly impossible.
- SAP version dependencies: Table structures change between releases. A query working in ECC6.0 might fail in S/4HANA where table structures have been flattened or moved to CDS views.
- Licensing and legal: SAP licenses typically prohibit direct database access. Violating this can have contract implications.
- Zero support: If something breaks, SAP support will refuse to help if they discover direct database access.
Safer Alternatives to Direct Access
If you're considering direct table access for performance reasons, explore these SAP-sanctioned alternatives first:
- CDS Views (S/4HANA): SAP's modern data modeling layer, exposable via OData
- OData Services: RESTful APIs that SAP generates from CDS views or Gateway services
- BW/4HANA or SAP Datasphere: Proper data warehousing solutions for analytics
- SAP Data Intelligence: For real-time data replication scenarios
- Custom Z-RFCs with optimized queries: Controlled direct reads wrapped in RFC functions
Performance Comparison
Let's examine realistic performance profiles for retrieving 10,000 material documents from a medium-sized SAP system:
| Method | Execution Time | Network Overhead | SAP App Server Load | Notes |
|---|---|---|---|---|
| BAPI (multiple calls) | 45-60 seconds | High | Medium-High | Full validation overhead |
| RFC_READ_TABLE (batched) | 15-25 seconds | Medium | Medium | Limited to 512-char rows |
| Custom Z-RFC (optimized) | 8-12 seconds | Low-Medium | Low-Medium | Best balance |
| Direct DB (HANA) | 2-4 seconds | Low | None (DB only) | No business logic, high risk |
For write operations, the performance gap narrows significantly. BAPIs remain the clear choice because the business logic overhead is unavoidable regardless of the approach.
Decision Matrix: Choosing Your Integration Method
Use this decision framework to select the appropriate integration method for your scenario:
| Scenario | Recommended Method | Justification |
|---|---|---|
| Creating sales orders from e-commerce | BAPI | Business object with complex validation; production-critical; BAPI exists |
| Posting financial documents | BAPI | Financial accuracy critical; audit trail required; BAPI_ACC_DOCUMENT_POST available |
| Reading custom Z-table configuration | RFC_READ_TABLE | Read-only; no BAPI; low security risk; acceptable performance |
| Bulk data extraction for analytics | Custom Z-RFC | Volume too high for standard RFCs; need controlled direct reads with authorization |
| Real-time inventory display on website | BAPI or Custom Z-RFC | Read-only but needs current stock logic (ATP, reservations); BAPI_MATERIAL_AVAILABILITY |
| Synchronizing master data to external DB | BAPI + Change Pointers/IDocs | Delta changes via events; BAPIs for initial load; proper change tracking |
| Dev/test data queries for debugging | RFC_READ_TABLE or Direct | Non-production; temporary; developer discretion |
| Historical reporting on 10M+ records | BW/Datasphere or Direct (approved) | Volume exceeds RFC practicality; SAP BI solution ideal; direct if approved and read-only |
| Triggering custom workflow logic | Custom Z-RFC | Unique business logic; no standard BAPI; controlled implementation |
| Updating employee master data | BAPI (BAPI_EMPLOYEE_ENQUEUE) | HR data security critical; compliance requirements; BAPI enforces authorization |
Security Best Practices Across All Methods
Regardless of your chosen integration method, implement these security fundamentals:
1. Credential Management
// Never hardcode credentials
// ❌ Bad
const sapConfig = {
user: 'SAPUSER',
passwd: 'Password123'
};
// ✅ Good - Use environment variables
const sapConfig = {
user: process.env.SAP_USER,
passwd: process.env.SAP_PASSWORD,
ashost: process.env.SAP_HOST,
sysnr: process.env.SAP_SYSTEM_NUMBER,
client: process.env.SAP_CLIENT
};
// ✅ Better - Use secret management services
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();
const [sapPassword] = await client.accessSecretVersion({
name: 'projects/my-project/secrets/sap-password/versions/latest'
}); 2. Principle of Least Privilege
Create dedicated SAP user accounts for each integration with minimal required authorizations:
- RFC users: Assign S_RFC authorization for specific function groups only
- Dialog users (avoid): Never use dialog users for system-to-system integration
- Communication users: Preferred for OData/HTTP integrations
- Regular permission reviews: Audit integration user authorizations quarterly
3. Connection Security
- Use SNC (Secure Network Communications): Encrypt RFC traffic with X.509 certificates
- Network segmentation: Integration servers should communicate through firewall rules allowing only required ports
- Connection pooling limits: Prevent DOS scenarios by limiting concurrent RFC connections
- Timeout configurations: Set appropriate timeouts to prevent hung connections
4. Input Validation and SQL Injection Prevention
Even with RFC, injection attacks are possible if you're building dynamic queries:
// ❌ Dangerous - SQL injection risk
async function getCustomerOrders(customerId) {
const whereClause = `KUNNR = '${customerId}'`;
return await readSAPTable('VBAK', ['VBELN', 'ERDAT'], whereClause);
}
// User input: "1000001' OR '1'='1"
// Results in: KUNNR = '1000001' OR '1'='1' (returns all orders!)
// ✅ Safe - Validated input
async function getCustomerOrders(customerId) {
// Validate customer ID format (SAP customer numbers are typically 10 digits)
if (!/^\d{10}$/.test(customerId)) {
throw new Error('Invalid customer ID format');
}
const whereClause = `KUNNR = '${customerId.padStart(10, '0')}'`;
return await readSAPTable('VBAK', ['VBELN', 'ERDAT'], whereClause);
} Real-World Integration Architecture
In production environments, you'll typically use a combination of methods. Here's a realistic e-commerce integration architecture:
// Integration Service Layer
class SAPIntegrationService {
// Use BAPI for transactional operations
async createOrder(orderData) {
return await this.callBAPI('BAPI_SALESORDER_CREATEFROMDAT2', {
/* order parameters */
});
}
async updateCustomer(customerId, changes) {
return await this.callBAPI('BAPI_CUSTOMER_CHANGE', {
/* customer changes */
});
}
// Use custom Z-RFC for optimized reads
async getCustomerOrderHistory(customerId, dateRange) {
return await this.callRFC('Z_GET_CUSTOMER_ORDERS', {
IV_CUSTOMER: customerId,
IV_DATE_FROM: dateRange.from,
IV_DATE_TO: dateRange.to
});
}
// Use RFC_READ_TABLE for configuration lookups
async getPricingConfiguration() {
const cachedConfig = await this.cache.get('pricing_config');
if (cachedConfig) return cachedConfig;
const config = await this.callRFC('RFC_READ_TABLE', {
QUERY_TABLE: 'ZTABLE_PRICING_CONFIG',
FIELDS: [{ FIELDNAME: 'KSCHL' }, { FIELDNAME: 'KBETR' }],
ROWCOUNT: 100
});
await this.cache.set('pricing_config', config, 3600); // 1 hour cache
return config;
}
// Generic BAPI wrapper with error handling
private async callBAPI(bapiName, parameters) {
try {
const result = await this.sapClient.call(bapiName, parameters);
const errors = result.RETURN?.filter(msg =>
msg.TYPE === 'E' || msg.TYPE === 'A'
) || [];
if (errors.length > 0) {
throw new BAPIError(errors);
}
await this.sapClient.call('BAPI_TRANSACTION_COMMIT', { WAIT: 'X' });
return result;
} catch (error) {
await this.sapClient.call('BAPI_TRANSACTION_ROLLBACK');
throw error;
}
}
} Monitoring and Observability
Production SAP integrations require robust monitoring. Track these key metrics:
- RFC connection pool utilization: Alert when pool reaches 80% capacity
- BAPI error rates: Track RETURN message types over time
- Transaction duration: P95 and P99 latency for each BAPI/RFC call
- SAP system availability: Heartbeat checks using RFC_PING or similar
- Business metrics: Orders created per hour, success/failure ratios
- Authorization failures: Track authorization check failures for security monitoring
// Observability wrapper
class ObservableSAPClient {
constructor(sapClient, metrics) {
this.sapClient = sapClient;
this.metrics = metrics;
}
async call(functionName, parameters) {
const startTime = Date.now();
const labels = { function: functionName };
try {
const result = await this.sapClient.call(functionName, parameters);
const duration = Date.now() - startTime;
this.metrics.recordHistogram('sap.rfc.duration', duration, labels);
this.metrics.incrementCounter('sap.rfc.calls', { ...labels, status: 'success' });
// Track BAPI return messages
if (result.RETURN) {
result.RETURN.forEach(msg => {
this.metrics.incrementCounter('sap.bapi.messages', {
function: functionName,
type: msg.TYPE,
id: msg.ID,
number: msg.NUMBER
});
});
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.metrics.recordHistogram('sap.rfc.duration', duration, labels);
this.metrics.incrementCounter('sap.rfc.calls', {
...labels,
status: 'error',
error_type: error.constructor.name
});
throw error;
}
}
} Conclusion: Making the Right Choice
Choosing between BAPI, RFC, and direct table access isn't about finding a universal "best" method—it's about matching the right tool to your specific requirements. Let's recap the key decision factors:
Choose BAPIs when: You need production-grade stability, SAP support, and comprehensive business logic. BAPIs are the gold standard for creating and modifying business documents, managing master data, and any scenario where long-term maintainability matters. The performance overhead is usually worth the peace of mind.
Choose RFCs when: No suitable BAPI exists, you need more granular control, or you're building custom integration logic. RFC_READ_TABLE excels at configuration lookups and custom table queries. Custom Z-RFCs give you BAPI-like control with tailored business logic. Just remember that with great power comes great responsibility—you own the stability and security.
Choose Direct Table Access when: You've exhausted all other options, obtained proper approvals, limited scope to read-only analytics, and accepted the risks. Even then, explore CDS views, OData services, or proper data warehousing solutions first. Direct access should be your last resort, not your first instinct.
In my experience building SAP integrations for over a decade, the most successful projects use a hybrid approach: BAPIs for transactional integrity, custom Z-RFCs for optimized reads, strategic caching to minimize SAP load, and comprehensive monitoring to catch issues before users do.
Start with BAPIs. Move to RFCs only when necessary. Avoid direct table access unless you have no other choice. Your future self—and your SAP team—will thank you.
Ready to dive deeper into SAP integration?
Explore our comprehensive SAP table documentation, integration tutorials, and best practice guides. Whether you're building your first RFC connection or optimizing existing BAPI calls, we've got you covered with practical, production-tested advice.
Browse SAP TablesFrequently Asked Questions
Can I use RFC_READ_TABLE in production?
Yes, but with significant caveats. Many organizations restrict or disable RFC_READ_TABLE due to security concerns. If your SAP security team permits it, limit its use to read-only scenarios, always specify ROWCOUNT, implement proper authorization checks in your application layer, and consider building custom Z-RFCs instead for better control and performance.
What's the difference between synchronous and asynchronous RFC?
Synchronous RFC (sRFC) waits for the function to complete before returning—your typical request-response pattern. Asynchronous RFC (aRFC) queues the call and returns immediately, useful for fire-and-forget scenarios. Transactional RFC (tRFC) guarantees exactly-once execution, critical for financial postings. Queue RFC (qRFC) adds ordering guarantees on top of tRFC.
How do I handle BAPI errors in my code?
Always check the RETURN parameter. BAPIs don't throw exceptions—they populate RETURN tables with message types: E (Error), A (Abort), W (Warning), I (Information), and S (Success). Filter for E and A types to determine if the operation failed. Never assume success without checking RETURN.
Is direct database access against SAP's terms of service?
Most SAP licenses prohibit direct database access without explicit approval. SAP's position is that the application layer exists for good reasons—business logic, data consistency, authorization checks. Some SAP products (like BW extractors) use approved direct reads, but for custom integrations, always use SAP-provided APIs unless you've received written approval from SAP.
Should I build custom Z-BAPIs or Z-RFCs?
The naming distinction is mostly semantic—what matters is the design. Build your custom functions following BAPI conventions: include RETURN parameter tables, implement proper authorization checks, avoid auto-committing (let the caller control), and document thoroughly. Whether you call it Z_BAPI_* or Z_RFC_* is less important than following these principles.