API Layer¶
Summary: The API layer uses
UEternalApiSubsystem(a GameInstance subsystem) to manage backend communication. Four service classes —UAccountApiService,UCharacterApiService,UPersistenceApiService, andUSessionApiService— inherit shared HTTP/JSON boilerplate fromUBaseApiService. All requests use per-request callbacks (FApiResponseDelegate) to avoid delegate collision. Data serializes automatically viaFJsonObjectConverter.
Table of Contents¶
- Why This Architecture
- API Architecture
- Request Lifecycle
- Service Contracts
- Data Serialization
- Error Handling
- Async Patterns
- Source References
- Related Systems
- Recent Changes
Why This Architecture¶
Design Goals¶
The API layer is designed around three principles:
- Non-Blocking — HTTP requests run asynchronously; game thread never waits
- Per-Request Isolation — Each call gets its own callback, no delegate collision
- Minimal Boilerplate — Services inherit HTTP/JSON handling from a shared base class
Previous Architecture (Removed)¶
| Old | New | Why |
|---|---|---|
UApiManager singleton with AddToRoot() |
UEternalApiSubsystem (GameInstanceSubsystem) |
Proper UE5 lifecycle, no manual GC management |
UAccountService / UCharacterService (standalone) |
UAccountApiService / UCharacterApiService / UPersistenceApiService inheriting UBaseApiService |
Shared HTTP boilerplate, consistent patterns |
DECLARE_DYNAMIC_MULTICAST_DELEGATE per endpoint |
Per-request FApiResponseDelegate (TDelegate) |
Concurrent requests no longer collide |
Manual ToJsonObject() per struct |
FJsonObjectConverter auto-serialization in base class |
Zero per-struct serialization code |
Service Separation¶
| Service | Responsibility |
|---|---|
UAccountApiService |
User authentication, account management |
UCharacterApiService |
Character CRUD operations |
UPersistenceApiService |
Save/load complete character state |
USessionApiService |
Game session CRUD (create, list, join, update, close) |
API Architecture¶
+------------------+
| Game Systems |
+------------------+
|
v
+---------------------------+
| UEternalApiSubsystem | <-- GameInstanceSubsystem (auto-lifecycle)
| - BaseUrl |
| - SetBaseUrl() / GetBaseUrl()
+---------------------------+
|
+--------------------+--------------------+--------------------+
| | | |
v v v v
+------------------+ +------------------+ +--------------------+ +------------------+
| UAccountApi | | UCharacterApi | | UPersistenceApi | | USessionApi |
| Service | | Service | | Service | | Service |
+------------------+ +------------------+ +--------------------+ +------------------+
| | | |
+--------------------+--------------------+--------------------+
|
v
+---------------------------+
| UBaseApiService | <-- Shared HTTP/JSON boilerplate
| - SendGet/Post/Put/Delete |
| - SerializeToJson<T>() |
| - DeserializeContent<T>() |
+---------------------------+
|
v
+---------------------------+
| FHttpModule |
| (Unreal HTTP) |
+---------------------------+
|
v
+---------------------------+
| Backend API |
| (REST JSON Endpoints) |
+---------------------------+
Subsystem Lifecycle¶
GameInstance created
|
v
UEternalApiSubsystem::Initialize()
|
+-- Set default BaseUrl ("http://localhost:5065/")
+-- Resolve ServerUrl (see configuration hierarchy below)
+-- Create AccountApiService, CharacterApiService, PersistenceApiService, SessionApiService
+-- Each service gets SetOwningSubsystem(this) for BaseUrl access
|
v
(Game session active — services available via GetSubsystem<UEternalApiSubsystem>())
|
v
UEternalApiSubsystem::Deinitialize()
|
+-- Null all service pointers (GC handles cleanup)
ServerUrl Configuration¶
The base URL for all API requests is resolved at startup with a three-level fallback:
1. Command-line override: -ServerUrl=https://...
|
| (not present?)
v
2. Config file: UPersistenceDevSettings::ServerUrl
(reads from DefaultGame.ini in ALL builds, not just editor)
|
| (empty?)
v
3. Hardcoded default: http://localhost:5065/
| Source | When it applies | Example |
|---|---|---|
-ServerUrl= CLI arg |
Packaged builds, EGS launch options, testing | -ServerUrl=https://api.eternal-server.net/ |
DefaultGame.ini |
All builds (editor + packaged) | ServerUrl="https://api.eternal-server.net/" |
| Hardcoded default | Local development fallback | http://localhost:5065/ |
This hierarchy allows remote testers to use a Cloudflare Tunnel URL by default (baked into DefaultGame.ini), while developers can override to localhost or any other URL via command line.
Request Lifecycle¶
Complete Flow¶
1. Game code calls service method
Api->GetPersistenceService()->SaveCharacter(Id, Data, Callback)
|
v
2. Service calls base class helper
SendPost("characters/{id}/save", SaveData, Callback)
|
v
3. UBaseApiService::SendRequest()
+-- Create TSharedRef<IHttpRequest>
+-- Set URL: GetBaseUrl() + Endpoint
+-- Set headers (Content-Type, User-Agent)
+-- SerializeToJson<T>(Body) → SetContentAsString()
+-- Bind OnHttpResponseReceived with per-request callback
+-- ProcessRequest() (async)
|
v (later, on game thread)
4. UBaseApiService::OnHttpResponseReceived()
+-- Check bWasSuccessful, Response validity
+-- Parse JSON from response body
+-- Build FApiResponse (StatusCode, bSuccess, Content, ErrorMessage)
+-- Callback.ExecuteIfBound(ApiResponse)
|
v
5. Caller's lambda/delegate receives FApiResponse
+-- Check bSuccess
+-- DeserializeContent<T>(Response, OutStruct) for typed data
Request Configuration¶
| Setting | Value | Purpose |
|---|---|---|
| User-Agent | X-UnrealEngine-Agent |
Identify client |
| Content-Type | application/json |
JSON body format |
| Thread Safety | ESPMode::ThreadSafe |
Safe async access |
| Base URL | http://localhost:5065/ |
Default, overridable via SetBaseUrl() |
Service Contracts¶
Account Service¶
| Endpoint | Method | Request | Response |
|---|---|---|---|
accounts |
POST | FCreateAccountRequest |
FAccountResponse |
accounts/login |
POST | FLoginRequest |
FAccountResponse |
Character Service¶
| Endpoint | Method | Request | Response |
|---|---|---|---|
characters |
POST | FCreateCharacterApiRequest |
FCharacterApiResponse |
characters/{id} |
GET | Path param | FCharacterApiResponse |
characters/account/{id} |
GET | Path param | TArray<FCharacterApiResponse> |
Persistence Service¶
| Endpoint | Method | Request | Response |
|---|---|---|---|
characters/{id}/save |
POST | FCharacterSaveData |
Success/error |
characters/{id}/load |
GET | Path param | FLoadCharacterResponse (wraps FCharacterSaveData) |
FItemSaveState payload (inside FCharacterSaveData.Items)¶
| Field | Type | Notes |
|---|---|---|
InstanceId |
FGuid |
Matches UItemObject::InstanceId |
ItemID |
FString |
Template ref — rebuilt via UItemRegistrySubsystem |
StackCount |
int32 |
Current stack |
ItemLevel |
int32 |
From FEquipmentFragment, 0 for non-equipment |
Modifiers |
FEquipmentModifiers |
Implicit/prefix/suffix/scaling arrays (JSONB server-side) |
ConsumableUsagesLeft |
int32 |
-1 when not a consumable |
StateModules |
TArray<FItemStateModuleSaveEntry> |
Polymorphic per-instance runtime state |
FItemStateModuleSaveEntry shape¶
Each entry wraps one state module attached to the item:
| Field | Type | Notes |
|---|---|---|
StructName |
FString |
Type tag, e.g. "RemnantItemState". Resolved on load via FindFirstObject<UScriptStruct>. |
Data |
FJsonObjectWrapper |
Nested JSON object carrying the module's UPROPERTY fields — NOT a text-export string. FJsonObjectConverter special-cases FJsonObjectWrapper to serialize as a first-class JSON object. |
Backend receives structured JSON, not opaque blobs. The wrapper shape enables server-side trade validation, analytics, and SQL migrations on state-module fields. See Item State Modules and Backend Server.
Session Service¶
| Endpoint | Method | Request | Response |
|---|---|---|---|
sessions |
POST | FCreateSessionRequest |
FSessionResponse |
sessions |
GET | — | TArray<FSessionResponse> |
sessions/{id} |
GET | Path param | FSessionResponse |
sessions/{id} |
PUT | FUpdateSessionRequest |
FSessionResponse |
sessions/{id}/heartbeat |
PUT | — | Success/error |
sessions/{id} |
DELETE | Path param | Success/error |
Request Struct Fields¶
FCreateAccountRequest
| Field | Type | Notes |
|---|---|---|
AccountId |
FGuid |
Client-generated, auto-serialized as string |
Email |
FString |
User email |
Username |
FString |
Display name |
Password |
FString |
Plaintext (HTTPS required) |
FLoginRequest
| Field | Type |
|---|---|
Email |
FString |
Password |
FString |
FCreateCharacterApiRequest
| Field | Type | Notes |
|---|---|---|
AccountId |
FGuid |
Parent account reference |
CharacterId |
FGuid |
Client-generated ID |
PlayerClassTemplateId |
FString |
Class selection |
Name |
FString |
Character name |
Response Struct Fields¶
FAccountResponse
| Field | Type |
|---|---|
AccountId |
FGuid |
Username |
FString |
Email |
FString |
Created |
FDateTime |
Updated |
FDateTime |
FCharacterApiResponse
| Field | Type |
|---|---|
AccountId |
FGuid |
CharacterId |
FGuid |
PlayerClassTemplateId |
FString |
Name |
FString |
CraftingResource |
int32 |
PlayTime |
float |
FApiResponse (generic wrapper returned to all callers)
| Field | Type | Notes |
|---|---|---|
bSuccess |
bool |
True for 2xx status codes AND body-level isSuccessful |
StatusCode |
int32 |
HTTP status code |
ErrorMessage |
FString |
Error details (empty on success) |
Content |
TSharedPtr<FJsonObject> |
Parsed JSON for further deserialization |
Data Serialization¶
Automatic Serialization¶
All request/response structs use UPROPERTY() fields and are serialized/deserialized automatically by FJsonObjectConverter in UBaseApiService:
Serialize (outgoing):
USTRUCT with UPROPERTY fields
→ FJsonObjectConverter::UStructToJsonObjectString()
→ FString JSON body
Deserialize (incoming):
FApiResponse::Content (TSharedPtr<FJsonObject>)
→ FJsonObjectConverter::JsonObjectToUStruct<T>()
→ Typed USTRUCT
GUID Format¶
All FGuid values serialize to strings using EGuidFormats::DigitsWithHyphens:
Caller-Side Deserialization¶
Services return FApiResponse with raw JSON. Callers deserialize into typed structs:
FApiResponseDelegate::CreateLambda([](const FApiResponse& Response)
{
FCharacterApiResponse Character;
if (Response.bSuccess && UBaseApiService::DeserializeContent(Response, Character))
{
// Use Character.Name, Character.CharacterId, etc.
}
});
Error Handling¶
Three-Level Validation¶
All error handling is centralized in UBaseApiService::OnHttpResponseReceived():
Level 1: Network
bWasSuccessful? Response valid?
|
| fail → FApiResponse{bSuccess=false, ErrorMessage="HTTP request failed"}
| pass
v
Level 2: Parse
JSON valid? FJsonObject created?
|
| fail → FApiResponse{bSuccess=false, ErrorMessage="Failed to parse JSON response"}
| pass
v
Level 3: HTTP Status
StatusCode 2xx?
|
| fail → Extract "message"/"Message" from JSON, or "HTTP {code}"
| pass
v
Level 4: Body-Level Success (TaskResult<T> wrapper)
JSON field "isSuccessful" present?
|
| yes → Use its value (backend may return 200 with isSuccessful=false)
| no → Trust HTTP status
v
Extract "content"/"Content" object, or use root JSON
v
FApiResponse{bSuccess, StatusCode, Content, ErrorMessage}
Callback Signature¶
All service methods accept the same callback type:
Per-request binding ensures concurrent calls never collide — each HTTP request captures its own FApiResponseDelegate by value.
Async Patterns¶
Per-Request Callback (Key Pattern)¶
// Old pattern (REMOVED) — one delegate for ALL callers:
OnCreateAccountResponseDelegate.Broadcast(bSuccess, Data, Error);
// Problem: if two systems call CreateAccount concurrently, both get both responses
// New pattern — each call gets its own callback:
Api->GetAccountService()->CreateAccount(Request,
FApiResponseDelegate::CreateLambda([this](const FApiResponse& Response)
{
// Only THIS request's response arrives here
}));
Thread Safety¶
| Concern | Solution |
|---|---|
| Request creation | ESPMode::ThreadSafe shared pointer |
| Callback execution | Main game thread (Unreal HTTP guarantee) |
| Service access | GameInstanceSubsystem (automatic lifecycle) |
| BaseUrl access | Services hold OwningSubsystem pointer → GetBaseUrl() |
Source References¶
API Subsystem¶
UEternalApiSubsystem—Source/ProjectEternal/Public/API/EternalApiSubsystem.h- Initialization —
Source/ProjectEternal/Private/API/EternalApiSubsystem.cpp
Base Service¶
UBaseApiService—Source/ProjectEternal/Public/API/Services/BaseApiService.h- HTTP/JSON handling —
Source/ProjectEternal/Private/API/Services/BaseApiService.cpp
Services¶
UAccountApiService—Source/ProjectEternal/Public/API/Services/AccountApiService.hUCharacterApiService—Source/ProjectEternal/Public/API/Services/CharacterApiService.hUPersistenceApiService—Source/ProjectEternal/Public/API/Services/PersistenceApiService.hUSessionApiService—Source/ProjectEternal/Public/API/Services/SessionApiService.h
Data Models¶
FApiResponse, request/response structs —Source/ProjectEternal/Public/API/Types/ApiTypes.hFCharacterSaveData(persistence payload) —Source/ProjectEternal/Public/Persistence/Types/PersistenceTypes.h
Related Systems¶
- Replication Overview — Network state sync
- Server Authority — Server validation patterns
- Item System — FGuid usage patterns
- Persistence architecture —
ImplementationDocs/PersistenceLayer.md
Recent Changes¶
| Date | Change | Reason |
| 2026-04 | FItemSaveState.StateModules carrying polymorphic per-instance runtime state as wrapper-JSON entries | Remnant state (and future mechanics like Corruption) round-trip through save/load with the backend able to query into the payload via JSONB operators — not opaque text-export |
| 2026-03-06 | Added ServerUrl configuration hierarchy (CLI → INI → default), session heartbeat endpoint (SendHeartbeat) | Remote tester support via Cloudflare Tunnel, session lifecycle management |
| 2026-03-03 | Added USessionApiService for game session CRUD; BaseApiService now checks body-level isSuccessful from TaskResult<T> wrapper; default base URL corrected to http://localhost:5065/ | Session management system for multiplayer "Game Room" model |
| 2026-03-02 | API architecture refactored: singleton UApiManager → UEternalApiSubsystem (subsystem pattern); broadcast delegates → per-request FApiResponseDelegate callbacks; standalone services → UBaseApiService inheritance pattern | Eliminates concurrent request collision; proper UE5 lifecycle; improved composition |
| - | Initial documentation | Document API layer architecture |
Future Considerations¶
| Enhancement | Benefit | Complexity |
|---|---|---|
| Authentication Headers | JWT/Bearer token support | Low |
| Request Retry Logic | Handle transient failures | Medium |
| Request Cancellation | Cancel in-flight requests | Low |
| Response Caching | Reduce repeated GET calls | Medium |
| Offline Queue | Buffer requests when offline | High |