| name | horse-mvc-architecture |
| description | Guide for structuring corporate Horse applications using Clean MVC (Model-View-Controller) principles and decoupling HTTP layers from business logic. |
Horse MVC Architecture
For large-scale, production-ready REST APIs, avoid placing business logic inside route handlers or keeping everything in the .dpr bootstrap file. Implement a clean, decoupled MVC (Model-View-Controller) architecture.
1. Project Directory Structure
Keep your project organized by dividing concerns into dedicated directories under the src/ folder:
my_project/
├── my_project.dpr
└── src/
├── controllers/ # Mappings of HTTP endpoints to service invocations
├── services/ # Core business rules, validations, and orchestration
├── repositories/ # Database access layer (SQL execution, FireDAC queries)
└── models/ # Entity classes and data structures (Domain)
2. Decoupling Rules (The Golden Rule)
To ensure testability and maintenance, never couple your business logic to the web framework.
- Rule: The
Service, Repository, and Model layers must never import Horse units or reference Horse objects (like THorseRequest or THorseResponse).
- Reason: If you decide to migrate from HTTP to a CLI application or gRPC, your business services and repositories remain 100% untouched.
3. Implementation Blueprint
A. The Repository Layer (Database Access)
Retrieves and updates raw database data. It knows nothing about HTTP.
unit Repository.Customer;
interface
uses
System.JSON, FireDAC.Comp.Client;
type
TCustomerRepository = class
public
class function FindById(const AId: Integer): TJSONObject;
end;
implementation
class function TCustomerRepository.FindById(const AId: Integer): TJSONObject;
var
LConnection: TFDConnection;
LQuery: TFDQuery;
begin
Result := nil;
LConnection := TFDConnection.Create(nil);
LQuery := TFDQuery.Create(nil);
try
LConnection.ConnectionDefName := 'MyPooledDef';
LConnection.Connected := True;
LQuery.Connection := LConnection;
LQuery.SQL.Text := 'SELECT id, name, email FROM customers WHERE id = :id';
LQuery.ParamByName('id').AsInteger := AId;
LQuery.Open;
if not LQuery.IsEmpty then
Result := LQuery.ToJSONObject; // Returns raw JSON entity
finally
LQuery.Free;
LConnection.Free;
end;
end;
end.