IBM i Architecture ยท REST API Integration

REST API Integration for IBM i: A Practical Guide

Last Updated: March 2026

"We need to expose our RPG programs as REST APIs." If you work with IBM i systems, you have heard this request. Your business needs to integrate with modern applications, mobile apps, and cloud services โ€” but your core business logic lives in RPG, COBOL, and CL programs that have run reliably for decades.

The good news? You do not need to rewrite everything. This article explores three practical approaches to REST API integration on IBM i, with real-world implementation details and lessons learned from actual modernization projects.

๐Ÿ” Why REST APIs Matter for IBM i

Modern applications expect REST APIs. Mobile apps require JSON responses. Cloud services communicate through HTTP endpoints. Partner integrations expect standard protocols. Microservices architectures rely on API communication.

Your IBM i system already contains the most valuable asset in your organization โ€” validated business logic that has evolved over years or decades. REST APIs make that logic accessible to the modern technology ecosystem without requiring a rewrite.

Traditional integration methods each carry significant limitations. Data queues are fast and reliable but IBM i-specific. File transfers are batch-oriented and unsuitable for real-time use. ODBC and JDBC provide database access but bypass business logic. Screen scraping is fragile and difficult to maintain.

๐Ÿ“Œ Key principle: Your IBM i platform already powers the core of your business. REST APIs simply make that power available to modern systems โ€” without touching the logic that makes it work.

โš–๏ธ Three Approaches at a Glance

ApproachComplexityPerformanceBest Use Case
Integrated Web Services (IWS)LowHighSimple services, IBM-standardized environments
Node.js API LayerMediumMediumModern integration layers, microservices
Pure RPG CGIHighVery HighLightweight or performance-critical workloads

1๏ธโƒฃ Approach 1: Integrated Web Services (IWS)

IBM's built-in web services framework is included with IBM i. It allows ILE programs to be exposed as REST or SOAP services without any additional runtime or infrastructure.

The workflow is straightforward: create an RPG service program, export procedures, deploy through the IWS server, and access through HTTP endpoints.

Example: Customer Lookup Service

RPGLE โ€” IWS Customer Lookup Service Program
**FREE
Ctl-Opt NoMain;

Dcl-Proc getCustomer Export;
  Dcl-Pi *N Varchar(2000);
    customerId Char(10) Const;
  End-Pi;

  Dcl-S jsonResponse Varchar(2000);

  // Use JSON_OBJECT for clean, structured output
  Exec SQL
    SELECT JSON_OBJECT(
        'customerId' VALUE CUSTNO,
        'name'       VALUE CUSTNAME,
        'address'    VALUE CUSTADDR,
        'city'       VALUE CUSTCITY,
        'zip'        VALUE CUSTZIP
    )
    INTO :jsonResponse
    FROM CUSTMAST
    WHERE CUSTNO = :customerId;

  If SQLCODE <> 0;
    jsonResponse = '{"error":"Customer not found"}';
  EndIf;

  Return jsonResponse;
End-Proc;
Best for: Organizations already standardized on IBM tooling that need simple, stable service exposure with no additional runtime to manage.
Limitations: Originally SOAP-oriented. REST support is functional but less flexible than modern frameworks. Limited API management capabilities.

2๏ธโƒฃ Approach 2: Node.js on IBM i

Node.js runs directly on IBM i and can call RPG programs using XMLSERVICE via itoolkit. This creates a modern API layer while leaving all business logic in RPG โ€” exactly where it belongs.

Client Application
โ†“
Node.js REST API
โ†“
XMLSERVICE / iToolkit
โ†“
RPG Programs
โ†“
DB2 for i

Example: Node.js API Layer

JavaScript โ€” Node.js calling RPG via iToolkit
const express = require('express');
const { Connection, ProgramCall } = require('itoolkit');

const app = express();
const conn = new Connection({
    transport: 'idb',
    transportOptions: { database: '*LOCAL' }
});

app.get('/api/customer/:id', async (req, res) => {
    try {
        const customerId = req.params.id;

        // Call RPG program directly
        const pgm = new ProgramCall('CUSTINQ', { lib: 'MYLIB' });
        pgm.addParam({ type: 'CHAR', length: 10, value: customerId });
        pgm.addParam({ type: 'CHAR', length: 2000, io: 'out' });

        conn.add(pgm);
        const results = await conn.run();

        res.json(JSON.parse(results[0].data));

    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

app.listen(3000);

Example: The Corresponding RPG Program

RPGLE โ€” CUSTINQ called by Node.js
**FREE

Dcl-Pi *N;
  customerId Char(10) Const;
  jsonResponse Char(2000);
End-Pi;

Exec SQL
  SELECT JSON_OBJECT(
      'customerId' VALUE CUSTNO,
      'name'       VALUE CUSTNAME,
      'address'    VALUE CUSTADDR,
      'city'       VALUE CUSTCITY,
      'zip'        VALUE CUSTZIP
  )
  INTO :jsonResponse
  FROM CUSTMAST
  WHERE CUSTNO = :customerId;

If SQLCODE <> 0;
  jsonResponse = '{"error":"Customer not found"}';
EndIf;

*InLR = *On;
Best for: Organizations adopting modern integration architectures, API platforms, or microservices strategies. Provides the best balance of flexibility, ecosystem, and maintainability for most modernization projects.

3๏ธโƒฃ Approach 3: Pure RPG REST APIs (CGI)

Some organizations prefer implementing APIs entirely in RPG. Apache on IBM i supports CGI programs that can generate HTTP responses directly โ€” no additional runtime required.

Client Application
โ†“
IBM HTTP Server (Apache)
โ†“
RPG CGI Program
โ†“
DB2 for i
RPGLE โ€” REST Endpoint via CGI
**FREE
Ctl-Opt DftActGrp(*No);

Dcl-S requestMethod Varchar(10);
Dcl-S pathInfo Varchar(256);
Dcl-S customerId Char(10);
Dcl-S jsonResponse Varchar(5000);

// Get HTTP request details from environment
requestMethod = %Str(getenv('REQUEST_METHOD'));
pathInfo = %Str(getenv('PATH_INFO'));

// Parse customer ID from URL path
customerId = %Subst(pathInfo: 15: 10);

// Query and build JSON response
Exec SQL
  SELECT JSON_OBJECT(
      'customerId' VALUE CUSTNO,
      'name'       VALUE CUSTNAME,
      'address'    VALUE CUSTADDR,
      'city'       VALUE CUSTCITY,
      'zip'        VALUE CUSTZIP
  )
  INTO :jsonResponse
  FROM CUSTMAST
  WHERE CUSTNO = :customerId;

If SQLCODE <> 0;
  jsonResponse = '{"error":"Customer not found"}';
EndIf;

// Write HTTP response
callp printf('Content-Type: application/json' + x'0a0a');
callp printf(%Trim(jsonResponse));

*InLR = *On;
Best for: Simple APIs or environments that want to remain pure IBM i with no additional runtimes. Uses existing RPG skillsets and gives full control over implementation.

๐Ÿ›ก๏ธ Architectural Best Practice: The Service Layer

Regardless of which approach you choose, one architectural principle applies to all three: APIs should never directly manipulate database tables. They should always call service programs or stored procedures.

API Layer (any approach)
โ†“
Service Programs / SQL Procedures
โ†“
DB2 for i Tables

This pattern provides centralized transaction control, consistent locking behavior, easier security management, and a single point to optimize, monitor, and audit. It also makes future refactoring significantly simpler.

Example: Order Entry with Full Transaction Control

RPGLE โ€” Service Program with Transaction Management
**FREE

Dcl-Proc createOrder Export;
  Dcl-Pi *N Varchar(2000);
    orderJson Varchar(5000) Const;
  End-Pi;

  Dcl-S orderId Char(15);
  Dcl-S customerId Char(10);
  Dcl-S resultJson Varchar(2000);
  Dcl-S customerCount Int(10);

  Exec SQL SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
  Exec SQL START TRANSACTION;

  // Parse customer ID from input JSON
  customerId = %Subst(orderJson: %Scan('"customerId":"': orderJson) + 14: 10);

  // Validate customer exists
  Exec SQL
    SELECT COUNT(*) INTO :customerCount
    FROM CUSTMAST
    WHERE CUSTNO = :customerId;

  If SQLCODE <> 0 Or customerCount = 0;
    Exec SQL ROLLBACK;
    Return '{"error":"Customer not found"}';
  EndIf;

  // Generate order and insert header
  orderId = generateOrderId();

  Exec SQL
    INSERT INTO ORDHDR (ORDNO, CUSTNO, ORDDATE, ORDSTATUS)
    VALUES (:orderId, :customerId, CURRENT DATE, 'PENDING');

  If SQLCODE <> 0;
    Exec SQL ROLLBACK;
    Return '{"error":"Failed to create order"}';
  EndIf;

  Exec SQL COMMIT;

  // Return result
  Exec SQL
    SELECT JSON_OBJECT('orderId' VALUE :orderId, 'status' VALUE 'confirmed')
    INTO :resultJson
    FROM SYSIBM.SYSDUMMY1;

  Return resultJson;
End-Proc;
๐Ÿ“Œ Key principle: All business logic, validation, and transaction management stays in RPG. The API layer only routes requests and formats responses.

๐Ÿ” Security Essentials

Two authentication approaches work well for IBM i APIs. JWT tokens are recommended for external-facing APIs; IBM i User Profile validation works well for internal APIs.

JWT Token Authentication

JavaScript โ€” JWT Login and Route Protection
const jwt = require('jsonwebtoken');

// Login endpoint โ€” generates token
app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;
    const user = await validateIBMiUser(username, password);

    if (!user) return res.status(401).json({ error: 'Invalid credentials' });

    const token = jwt.sign(
        { username: user.username, role: user.role },
        process.env.JWT_SECRET,
        { expiresIn: '1h' }
    );
    res.json({ token });
});

// Middleware โ€” validates token on protected routes
const authenticateJWT = (req, res, next) => {
    const token = req.header('Authorization')?.replace('Bearer ', '');
    if (!token) return res.status(401).json({ error: 'Access denied' });

    try {
        req.user = jwt.verify(token, process.env.JWT_SECRET);
        next();
    } catch {
        res.status(400).json({ error: 'Invalid token' });
    }
};

app.use('/api/protected', authenticateJWT);
Always use HTTPS in production. Every API endpoint must run over TLS. Never transmit credentials or tokens over unencrypted connections.

โšก Performance Optimization

Connection Pooling

JavaScript โ€” iToolkit Connection Pool
const { ConnectionPool } = require('itoolkit');

const pool = new ConnectionPool({
    transport: 'idb',
    transportOptions: { database: '*LOCAL' },
    poolSize: 10
});

app.get('/api/customer/:id', async (req, res) => {
    const conn = await pool.acquire();
    try {
        const results = await conn.run();
        res.json(results);
    } finally {
        pool.release(conn); // Always release back to pool
    }
});

Redis Caching

JavaScript โ€” Redis Cache Layer
app.get('/api/customer/:id', async (req, res) => {
    const cacheKey = `customer:${req.params.id}`;

    // Check cache first โ€” no IBM i hit at all
    const cached = await client.get(cacheKey);
    if (cached) return res.json(JSON.parse(cached));

    // Cache miss โ€” fetch from IBM i
    const customer = await getCustomerFromIBMi(req.params.id);

    // Store for 5 minutes
    await client.setEx(cacheKey, 300, JSON.stringify(customer));

    res.json(customer);
});

Async Processing for Long-Running Jobs

JavaScript โ€” Submit and Poll Pattern
// Submit batch job โ€” return immediately
app.post('/api/reports/generate', async (req, res) => {
    const reportId = generateReportId();
    await submitJob(`CALL PGM(GENRPT) PARM('${reportId}')`);

    res.status(202).json({
        reportId,
        status: 'processing',
        statusUrl: `/api/reports/${reportId}/status`
    });
});

๐Ÿšจ Common Pitfalls to Avoid

1 โ€” Character Encoding

Always handle CCSID conversion when moving data between IBM i (EBCDIC) and JSON (UTF-8). Failure to do so produces garbled output that is difficult to debug.

2 โ€” Large Result Sets

Always paginate API responses. Never return unbounded result sets โ€” they will eventually break under production load.

JavaScript โ€” Pagination Pattern
app.get('/api/customers', async (req, res) => {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 50;
    const offset = (page - 1) * limit;

    const customers = await getCustomers(limit, offset);

    res.json({
        page, limit,
        data: customers,
        nextPage: `/api/customers?page=${page + 1}&limit=${limit}`
    });
});

3 โ€” Exposing Internal Errors

โŒ Never expose internal error details to API consumers
// BAD โ€” exposes internal stack trace
app.use((err, req, res, next) => {
    res.status(500).json({ error: err.message, stack: err.stack });
});
โœ… Log internally, return generic error externally
// GOOD โ€” log full error, return safe response
app.use((err, req, res, next) => {
    console.error('API Error:', err); // Full detail in logs only
    res.status(500).json({ error: 'Internal server error', requestId: req.id });
});

๐Ÿ’ก Conclusion

REST API integration on IBM i does not require rewriting decades of business logic. You have three viable paths โ€” and for most modernization projects, the Node.js approach provides the best balance of flexibility, ecosystem support, and long-term maintainability.


Whichever approach you choose, keep these principles consistent:


Your IBM i platform already powers the core of your business. REST APIs simply make that power available to modern systems.