How to Call an RPG Program from a REST API
Three ways to put a REST API in front of an existing RPG program on IBM i, with a working pattern using a Db2 for i stored procedure and Node.js.
Your RPG programs already know how to price an order, check credit or validate a shipment. The problem is that the website, the CRM and your trading partners can't call them. You don't need to rewrite that logic to fix it. You need a thin, safe layer that turns an HTTP request into a program call and the result back into JSON.
Three ways to do it
- IBM Integrated Web Services (IWS). Built into IBM i. From the Web Administration for i console you deploy an ILE program or service program as a REST service. It is quick for simple request and response shapes, and the program needs PCML information generated at compile time (for example
PGMINFO(*PCML *MODULE)). - A Db2 for i stored procedure plus an API layer. Describe the program to Db2 as an external procedure, then call it from a small service in Node.js, .NET or Python that exposes the REST endpoint. This is the pattern we use most often, and the one shown below.
- Toolkits that call programs directly. XMLSERVICE and language wrappers such as itoolkit for Node.js call programs without SQL. They are useful when a program has complex parameter structures.
Why the stored procedure route
It works with any language that can talk to Db2 for i, access is controlled with ordinary database authority, you can test it from Run SQL Scripts before any web code exists, and the RPG program doesn't change at all.
Step 1: describe the program to Db2
Here, ORDLIB/ORDSTS is an existing RPG program that takes an order number and returns a status code and the order total.
-- 1. Describe the existing RPG program to Db2 for i.
-- The parameter types must match the program's entry parameters.
CREATE OR REPLACE PROCEDURE APILIB.GET_ORDER_STATUS (
IN ORDER_NO CHAR(10),
OUT STATUS CHAR(1),
OUT ORDER_TOTAL DECIMAL(11, 2)
)
LANGUAGE RPGLE
PARAMETER STYLE GENERAL
EXTERNAL NAME 'ORDLIB/ORDSTS';Step 2: return a result set
Output parameters are handled differently by different drivers. Wrapping the call in an SQL procedure that returns one row makes the result look like any other query, which every client handles the same way.
-- 2. Wrap it so callers get a normal result set (one row).
CREATE OR REPLACE PROCEDURE APILIB.ORDER_STATUS (
IN ORDER_NO CHAR(10)
)
DYNAMIC RESULT SETS 1
LANGUAGE SQL
BEGIN
DECLARE V_STATUS CHAR(1);
DECLARE V_TOTAL DECIMAL(11, 2);
DECLARE C1 CURSOR WITH RETURN TO CLIENT FOR
SELECT V_STATUS AS STATUS, V_TOTAL AS ORDER_TOTAL
FROM SYSIBM.SYSDUMMY1;
CALL APILIB.GET_ORDER_STATUS(ORDER_NO, V_STATUS, V_TOTAL);
OPEN C1;
END;Test it in Run SQL Scripts before writing any web code:
CALL APILIB.ORDER_STATUS('A123456789');Step 3: expose it over HTTP
// 3. A small REST API in Node.js using Mapepire (IBM's open-source Db2 for i client).
import express from "express";
import { SQLJob } from "@ibm/mapepire-js";
const job = new SQLJob();
await job.connect({
host: process.env.IBMI_HOST,
user: process.env.IBMI_USER, // a service profile with access to APILIB only
password: process.env.IBMI_PASSWORD,
});
const app = express();
app.get("/orders/:orderNo/status", async (req, res) => {
const orderNo = String(req.params.orderNo);
if (!/^[A-Z0-9]{1,10}$/i.test(orderNo)) {
return res.status(400).json({ error: "Invalid order number" });
}
const result = await job.execute("CALL APILIB.ORDER_STATUS(?)", {
parameters: [orderNo],
});
const row = result.data?.[0];
if (!row) return res.status(404).json({ error: "Order not found" });
res.json({ orderNo, status: row.STATUS, total: Number(row.ORDER_TOTAL) });
});
app.listen(8080);Where to go next
Once one program is reachable this way, the same pattern covers the next one, and the API becomes the place where portals, integrations and AI agents get IBM i data, while the RPG logic stays the system of record.
IBM i, Infor and other proven systems, extended without a rewrite.
Learn more →