DEV Community

Cover image for Replacing an ERP interface? Define API contracts, keep business rules and permissions on the server
Oleksandr Sainchuk
Oleksandr Sainchuk

Posted on

Replacing an ERP interface? Define API contracts, keep business rules and permissions on the server

An ERP frontend migration can keep every endpoint and still break the business. Picture a new release where the stock table labels on-hand inventory as "available." A new form skips a validation that used to live in the old browser code. And the submit button shows a nice green toast, and the operator still has no idea whether the transfer posted.

None of these show up in an API diff. So when you modernize an ERP frontend, the API schema is only half the agreement. The other half is the workflow: which records people act on, what the numbers on screen mean, and which server response confirms that an action happened.

To keep this concrete, we'll follow one small workflow the whole way: a warehouse operator reviews stock and posts a transfer.

Start with one complete workflow

Pick a workflow with a clear start and a visible business result. In our example, the operator finds an item in a source warehouse, checks availability, picks a destination and submits a quantity. The ERP either accepts the transfer or says why it can't.

Before you design anything, watch that workflow in the current app. Ask an operator to show you a normal transaction and one that needs intervention. Open the browser's network tab and look at the code that builds each request. Note any spreadsheet or manual step people use to finish the job.

You'll find rules that look strange. A waiting period might enforce an approval policy, or it might be left over from a batch job nobody runs anymore. To tell them apart, find the business owner and ask what the rule is for. Keep the behavior until that person agrees on a change and how to accept it.

A short note per uncertain rule is enough to start: what you saw, where it's implemented, who owns it, why it exists, and what you decided. Now engineering has something concrete to verify.

At UITOP we redesigned Slabstack, a CRM for heavy construction materials producers, around how companies, projects and daily operations connect. The same habit helps in ERP work: map those operational relationships first, and the scope of each workflow becomes much clearer.

Write the contract around the workflow

For the stock transfer, write down these agreements:

Transfer contract: The data, permissions, and outcome rules to agree on before building an inventory transfer workflow

Settle these with the people who own the ERP rules. A frontend team can only show a useful distinction if it knows what the backend values mean.

Describe the HTTP side with OpenAPI: parameters, request bodies, response schemas and known errors. Add field descriptions for units and business meaning. Keep the workflow acceptance scenarios next to the spec, including the document or ledger state each one should produce.

One trap to watch: a response can keep the same shape while a field quietly changes meaning. That's still a contract change, so review it like one.

Find the rules hiding in the old frontend

Old screens do more than display data. They prepare quantities, fill hidden fields, pick a document type and run validation before sending a request. New markup doesn't bring any of that along by itself.

So list the client scripts and the events that trigger them. Trace what each one calculates, what it only displays, and which rules the server also checks. Don't skip the rare paths, like cancelling a document or reopening a rejected request.

Frappe is a good real-world example. Its docs note that Client Script validations run through the standard browser form. If a rule must also apply to API access, it needs server-side validation. A separate React frontend skips that browser form entirely, so it needs a plan for every rule that lived there.

As you go, give each rule an owner and a home. Presentation logic can stay in the frontend. Business validation and authorization belong on the server too.

Put an adapter between the ERP and your components

Keep the ERP's field names at one defined boundary. Your components then work with a small model whose names match the interface.

Below is the full adapter for our example. The contract sends quantities as plain decimal strings. Strings keep the precision the ERP supplied, and the adapter does no stock math of its own.

export type StockSnapshot = Readonly<{
  sku: string;
  warehouseId: string;
  onHand: string;
  available: string;
  unit: string;
  revision: string;
}>;

function text(value: unknown, field: string): string {
  if (typeof value !== "string" || value.trim() === "") {
    throw new TypeError(`Expected a non-empty string: ${field}`);
  }
  return value;
}

function quantity(value: unknown, field: string): string {
  const result = text(value, field);
  if (!/^-?\d+(?:\.\d+)?$/.test(result)) {
    throw new TypeError(`Expected a decimal string: ${field}`);
  }
  return result;
}

export function decodeStockSnapshot(input: unknown): StockSnapshot {
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
    throw new TypeError("Expected a stock response object");
  }
  const row = input as Record<string, unknown>;
  return {
    sku: text(row.item_code, "item_code"),
    warehouseId: text(row.warehouse_code, "warehouse_code"),
    onHand: quantity(row.qty_on_hand, "qty_on_hand"),
    available: quantity(row.qty_available, "qty_available"),
    unit: text(row.stock_uom, "stock_uom"),
    revision: text(row.record_version, "record_version"),
  };
}
Enter fullscreen mode Exit fullscreen mode

Say the response has qty_on_hand: "148.000" and qty_available: "118.000". The interface gets both values and shows the ERP's availability as is. The frontend doesn't calculate it, because the ERP's number can include rules beyond "on hand minus reserved," and the frontend doesn't know them.

If availability is missing, the adapter throws. Showing zero would be worse: it turns a broken contract into wrong inventory data that someone might act on. The caller should catch the error and show a loading error that fits the workflow.

The example tests cover decimal precision, missing and malformed fields, identifiers, revision handling and extra response fields. All ten local tests pass. Keep their scope in mind, though: they test this response boundary. They don't call a running ERP or prove that a transfer posts.

Use whatever validation your codebase already has. A schema library or a generated client can replace these small helpers. What matters is that runtime response checking has a clear place in the app.

Keep authorization on the server, at every entry point

A simpler interface can hide advanced modules and show fewer actions. That's fine for the UI. The API still has to check what the current user can do.

Test warehouse access through the list, the record detail, the transaction endpoint and reports. Include a user who can view a warehouse but can't move its stock, and a user who belongs to several departments. A filtered dropdown can't enforce any of this. OWASP's authorization guidance recommends checking permissions on every request.

If the new frontend runs on a separate origin, write down how it authenticates and keeps the session alive. Check the ERP's supported browser auth flow, its cookie and CSRF requirements, and the allowed origins. Remember that CORS only controls which browsers can read responses. It doesn't give a user permission to any business record. And keep privileged integration credentials on the server, never in browser code.

Add session expiry to your workflow tests too. If auth expires in the middle of a transfer, the operator needs a clear way to recover.

Decide on stale data and retries before release

Two operators can work with the same stock while a transfer form sits open. The quantity that was available when the form loaded may be gone by the time someone clicks submit.

If the API supports version checks, send the version the operator reviewed and let the server check it in the same atomic step as the write. HTTP APIs can do this with an ETag and [If-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-Match), and a failed precondition returns 412. Other systems use an explicit version field. Either way, the header only works if the server supports it.

A timeout is a different problem. The server may have committed the transfer even though the browser never got a confirmation. So decide how the client looks up the outcome. If the API supports idempotent writes, reuse the same operation ID when you retry, and let the server handle duplicates. AWS walks through this in its guide to idempotent APIs.

Disabling the submit button helps with double clicks. It doesn't replace retry and duplicate rules on the write API.

Test the business result after each action

Use representative, access-controlled test records, and check what the ERP recorded after each action. Check the resulting document and stock state along with the success message, because that state shows what happened.

Transfer regression checks: Scenarios to test so transfers handle approvals, stock limits, access, conflicts, and uncertain outcomes correctly

Playwright can run API checks alongside browser steps, including checks on server-side results. Run integration tests against the ERP for anything a mocked response can't prove. Keep UI-state and adapter tests for their narrower jobs.

Compare old and new outputs for a stable set of inputs. When they differ, take it to the business owner. If it's an intentional fix, it gets its own decision and acceptance criteria. Just don't keep a known bug alive only to make the outputs match.

Roll out one workflow and keep a way back

Decide which users and routes see the new interface, what stays in the old app, and who can pause the rollout. Check deep links, navigation to related records, shared auth and existing exports.

A route or feature flag lets people switch back to the old interface. Make sure the old app can still read records created in the new one. And remember that rolling back a frontend release doesn't undo posted transfers or reverse a schema change. Those need their own recovery plan.

Your first scope should name one workflow, its dependencies, the checks it must pass and the evidence you need before expanding. This is also a good test for a modernization partner: ask how they'll find the existing rules and how they'll prove the result in the working system.

At UITOP we designed and built LogiCore, a logistics and warehouse application that connects shipment monitoring, warehouse structures and inventory, with a React/TypeScript frontend on a REST backend. The lesson carries over to ERP work: interface decisions and operational data are tied together, and when the frontend changes, those ties need to stay visible.

You could wonder right now:

Can we add a modern frontend while keeping the existing ERP backend?

Yes, if the backend already exposes the operations and access controls the new interface needs. Start with one bounded workflow and verify reads, writes, authentication and business validation. If the API has gaps, scope the server or integration work together with the frontend.

How do we tell real business rules from historical workarounds?

Walk through the behavior with an operator and the business owner responsible for the outcome. Check the code, integrations and downstream reports. Write down why you keep, change or remove it. At UITOP, the design question we ask first is which operational dependency the interface must preserve, because visual simplification only works with that context.

Can ERP upgrades break existing custom scripts?

Yes, when scripts depend on fields, events, APIs or libraries that changed. List those dependencies and test the target version in staging. If you're replacing a standard form with a custom frontend, also check which validations ran only in the original browser form and which the server enforces.

How should warehouse access be isolated between departments?

Define which records and actions each department membership allows, then enforce that policy on the server. Test detail views, writes and reports along with lists, and include users who belong to several departments. Interface filters should reflect the policy, but every endpoint needs its own access check.

How should a standalone React frontend authenticate with an existing ERP?

Use the ERP's supported browser authentication, and document session expiry, CSRF protection and allowed origins. Check the user's permissions on every API request. Never put privileged integration credentials in browser code. A working CORS setup is only one part of the picture, next to authentication and authorization.

Why do ERP data problems often show up late in UAT?

Early checks often confirm that fields load and miss whether the records work together to complete a transaction. Test full transactions and downstream reports with business users earlier. At UITOP, we treat the operational result, like the resulting stock document, as part of acceptance, next to the screen the user sees.

Which ERP workflow would you modernize first, and what business result would tell you the replacement works?

Top comments (0)