Many business applications that are considered legacy are not unusable systems. Instead, they often contain years of rules, operational exceptions, integrations, and domain knowledge that continue to produce value every day.
The problem arises when they need to communicate with something new: a customer portal, a mobile app, a cloud service, an external partner or a new module built with modern technologies.
In these cases, the choice is often presented as an extreme alternative: leave everything as it is or rewrite the application completely. However, there is a third, more pragmatic way: introducing an API layer that exposes selected features and creates a stable boundary between the existing system and new consumers.
Why a complete rewrite isn't always the first choice
Rewriting from scratch may seem like the cleanest approach, but it means rebuilding not just the code, but years of accumulated application logic and behavior. Many rules are not documented: they live in queries, procedures, validations, conditions and special-case handling introduced over time.
A complete rewrite also involves concrete risks:
- long lead times before delivering value;
- double maintenance during the transition;
- regressions on poorly documented edge cases;
- complex migration of data and processes;
- the need to realign all integrations;
- difficulty comparing old and new behavior.
This does not mean that rewriting is always wrong. It means that it must be an economic and architectural decision, rather than an automatic reaction to the age of the codebase.
Helpful question
Is the real goal to replace the entire system immediately or to allow the business to use new features without interrupting what already works?
Use APIs as a modernization boundary
An API layer can become the integration boundary between the legacy system and the rest of the ecosystem. New consumers don't depend directly on tables, internal formats, or implementation details—they use explicit, versionable contracts.
[Web portal] [Mobile app] [Partners / Services]
\ | /
\ | /
[API ASP.NET Core]
- authentication
- authorization
- validation
- DTO contracts
- logging and auditing
|
v
[Adapter / Application Layer]
|
v
[Legacy system]
- existing business logic
- database
- internal services
The API should not become a thin HTTP wrapper around the database. Its job is to represent clear use cases and operations for new systems, shielding the internal model from direct coupling.
Before Exposing Functionality, Understand Where the Logic Lives
In an older application, the logic can be spread across several places:
- code-behind or very large controllers;
- static helper classes;
- stored procedures;
- SQL triggers;
- UI events;
- scheduled jobs;
- Windows services or batch processes;
- undocumented external integrations.
Before building the API, you need to reconstruct the complete flow of the use case. Exposing only the most visible method can bypass checks, side effects, or rules that are executed elsewhere today.
An initial assessment should identify inputs, outputs, transactions, side effects, external dependencies, authorizations, and error-handling behavior.
Which features to expose first
Not all areas of the system have the same complexity or value. It is usually best to start with use cases with clear boundaries and controllable risk.
| Feature Type | Initial Priority | Reason |
|---|---|---|
| Read-only queries | High | Lower operational risk and easy-to-verify contracts |
| Operations already isolated in services | High | Clearer dependencies and responsibilities |
| Operations involving local transactions | Medium | Relatively predictable outcome and failure handling |
| Distributed or lengthy processes | Medium-low | Require state, retry, and observability |
| Functions tightly coupled to the UI | Low | They must first be separated from UI behavior |
Starting from a limited area allows you to validate the architectural model, define common standards and measure the real cost of integration before extending it to the rest of the system.
Do not expose the database directly
When a new system needs to read data from a legacy application, the most common shortcut is to connect it directly to the database. It's fast, but creates coupling that is difficult to remove.
The new consumer begins to depend on:
- names and structure of tables;
- technical values and internal codes;
- undocumented relationships;
- intermediate states not designed for the outside;
- rules that the database alone does not represent;
- future schema changes.
An API allows you to expose a stable contract, apply permissions, filter fields and centralize logging and validation.
Frequent risk
Read-only SQL access seems harmless, but over time it can become a critical dependency that prevents you from changing the schema, types, and logic of your legacy system.
Create an adapter between API and existing system
Controllers should not call dozens of legacy classes directly. It is preferable to introduce an adapter layer that translates modern contracts into the patterns and calls required by the existing system.
public interface ILegacyCustomerGateway
{
Task<CustomerSnapshot> GetAsync(
long customerId,
CancellationToken cancellationToken);
Task<OperationResult> UpdateContactDataAsync(
UpdateContactDataCommand command,
CancellationToken cancellationToken);
}
public sealed class CustomerApplicationService
{
private readonly ILegacyCustomerGateway _legacyGateway;
public CustomerApplicationService(
ILegacyCustomerGateway legacyGateway)
{
_legacyGateway = legacyGateway;
}
public async Task<CustomerResponse> GetAsync(
long customerId,
CancellationToken cancellationToken)
{
var customer = await _legacyGateway.GetAsync(
customerId,
cancellationToken);
return CustomerResponse.From(customer);
}
}
The interface defines what the new application layer requires. The implementation can use existing code, internal calls, stored procedures, or legacy services without exposing these details to the outside world.
This approach also allows you to gradually replace the legacy implementation while while keeping the consumer-facing contract unchanged.
The modern API communicates with an adaptation layer that isolates legacy logic. When existing libraries are compatible with .NET Standard or the modern runtime, the adapter can invoke them directly. If they depend on the .NET Framework, the adapter must remain in a separate .NET Framework process and be invoked over HTTP, messaging, or interprocess communication.
Define API contracts that are independent of the legacy model
DTOs exposed by the API should not automatically replicate existing system classes or tables. They must represent the consumer's use case.
A legacy model can contain:
- technical fields that are not useful;
- historical flags;
- codes that are difficult to interpret;
- duplicate data;
- properties with names related to the implementation;
- information that should not leave the system.
A separate API contract makes it possible to normalize names, document meanings, and introduce future versions without immediately changing the internal domain.
Security and permissions need to be rethought
Many legacy applications rely on the internal network or UI for security. When a function is exposed via APIs, that assumption is no longer sufficient.
For each endpoint, you need to define:
- who can authenticate;
- which client is calling;
- which operations it can perform;
- which resources it can act on;
- which data must be filtered;
- which actions must be audited;
- how to revoke or restrict access.
Authentication and authorization should not be added as a final check. They must be built into the design of the contract and use case.
Handle errors and differences in behavior
A legacy system may report errors through generic exceptions, numeric codes, null values, or interface-designed messages. The API must translate these behaviors into consistent HTTP responses.
| Internal outcome | Possible API response | Note |
|---|---|---|
| Invalid data | 400 Bad Request | Return clear errors by field or validation rule |
| Resource not found | 404 Not Found | Do not expose internal technical details |
| Not allowed | 403 Forbidden or 409 Conflict | The appropriate status depends on the type of constraint |
| Transient error | 503 Service Unavailable | Consider retries and the Retry-After header |
| Unexpected error | 500 Internal Server Error | Use correlation IDs and structured logs |
Standardizing errors avoids the need for each consumer to interpret messages and codes specific to the legacy system.
Long-Running Processes: Do Not Keep the HTTP Request Open
Some legacy functions perform lengthy processing, call external services, or depend on scheduled jobs. Exposing them with a synchronous endpoint can produce timeouts and unpredictable behavior.
In these cases, the API can:
- validate the request;
- record a command;
- start asynchronous processing;
- return an identifier;
- provide an endpoint for checking the status.
Execution can be managed through robust workflows in .NET, separating the HTTP contract from the actual duration of the process.
Observability: Tracing Requests Across Legacy and Modern Components
During incremental modernization, you need to trace a request across both systems. A correlation ID should link:
- consumer request;
- API endpoints;
- application service;
- legacy adapter;
- query or procedure executed;
- any external services;
- final response.
Latency, errors, and dependencies must be measured. Without observability, the API risks hiding complexity rather than making it governable.
How to Prevent the API Layer from Becoming a New Monolith
The opposite risk is shifting all the complexity to a new ASP.NET Core project. Oversized controllers, generic services, and direct access to each table simply create a second system that is difficult to maintain.
To avoid this, it is advisable to:
- organize endpoints by use case;
- keep controllers thin;
- define specific adapters;
- separate queries from commands;
- centralize authorizations and errors;
- introduce contract tests;
- measure remaining dependencies on the legacy system.
The new layer should reduce the coupling, not duplicate it.
An incremental migration strategy
A practical roadmap can be divided into phases:
| Phase | Activities | Result |
|---|---|---|
| 1. Assessment | Mapping logic, dependencies, and use cases | Known boundaries and risks |
| 2. First API layer | Expose isolated read operations or functions | New consumers without direct access to the legacy system |
| 3. Standardization | Security, errors, logging, versioning | A common foundation for subsequent APIs |
| 4. Incremental extraction | Replacing individual implementations | Legacy dependencies progressively reduced |
| 5. Targeted retirement | Remove components that are no longer used | Simpler system without a big-bang rewrite |
This strategy is consistent with the progressive modernization of .NET Framework applications: each phase should produce a verifiable benefit and reduce the risk of subsequent phases.
When the API layer is not enough
An API layer is useful, but it doesn't automatically solve all problems. It may not be enough when:
- the logic is tightly coupled to the UI;
- the system does not support concurrent operations;
- performance is already critical;
- the database contains structural inconsistencies;
- tests are missing and behavior cannot be reproduced reliably;
- the infrastructure cannot be made secure or observable.
In these cases, the preliminary work may require targeted refactoring, separation of logic, database stabilization, or the introduction of characterization tests.
Common Mistakes
1. Expose tables as endpoints
An API contract should represent use cases, not be an HTTP version of the data schema.
2. Call the legacy system directly from controllers
Without a level of adaptation, the new project inherits all dependencies and becomes difficult to test.
3. Bypass permissions and audits
An internal function may require completely different controls when it is made accessible to new clients.
4. Turn every process into a synchronous operation
Long processing should be modeled with state and asynchrony.
5. Not defining a versioning strategy
New consumers will create dependencies. Contracts must be able to evolve without unexpected breaking changes.
6. Create APIs without a migration plan
The integration layer must progressively reduce technical debt, not become a permanent addition without direction.
Technical Checklist
Analysis
- Has the complete use-case flow been reconstructed?
- Are the dependencies known?
- Are side effects documented?
- Are the risks of regression measurable?
Contracts
- Are the DTOs independent of the database model?
- Are the errors consistent?
- Is versioning defined?
- Is the documentation useful to API consumers?
Security
- Are authentication and permissions explicit?
- Is the exposed data minimized?
- Are critical operations audited?
- Can credentials be revoked?
Operations
- Is there a correlation ID?
- Are latency and errors monitored?
- Are long processes asynchronous?
- Can issues be isolated between the API and the legacy system?
FAQ
Can I create modern APIs on top of a .NET Framework application?
Yes. A separate ASP.NET Core project can expose modern contracts and communicate with the existing system through libraries, internal services, databases, or dedicated adapters, depending on the technical constraints.
Is it better to read the legacy database directly?
It can be useful in controlled and temporary cases, but it should not become the stable contract of new consumers. An application layer reduces coupling and enforces rules and permissions.
Do I need to rewrite the logic before creating the API?
Not necessarily. It can be encapsulated behind an adapter and progressively separated. First, however, it is necessary to check where the rules and side effects actually reside.
How do I manage legacy functions that take several minutes to complete?
The API should initiate asynchronous processing, return an identifier, and allow clients to check its status, rather than keeping the HTTP request open.
Does an API layer automatically modernize the legacy system?
No. It creates a useful boundary, but it must be accompanied by security, observability, testing, and a dependency reduction roadmap.
In summary
Modernizing doesn't necessarily mean rewriting everything.
A well-designed API layer can shield the legacy system, prevent new direct access to the database, and enable modern portals, apps, and services to use existing capabilities through secure, versionable contracts.
Related Guides
Need to integrate a legacy .NET system with new applications?
I can support software houses and development teams with analyzing the existing system, designing of the API layer and defining an incremental, sustainable modernization roadmap.