API — Overview
BaseUrl, ApiResult envelope, LolApiError, ResponseMetadata, validation, CORS and Jackson MixIn — pattern index.
BaseUrl
All LoL APIs expose three prefixes (Spring @RequestMapping, src/main/java/com/safjnest/spring/controller/):
| Prefix | File:line | Content |
|---|---|---|
/api/lol/{shard} | LolController.java:36 | profile, matches, rank-history, activity, matchups, livegame, match |
/api/lol | LolController.java:47, LeaderboardController.java:15, ChampionController.java:16, ProfileIndexableController.java:13, RecordsController.java:16, AiTrainingController.java:20 | search, leaderboard, champion, records, AI training, indexables |
/api | StatusController.java:11 | global status |
{shard} is LeagueShard (no.stelar7.api.r4j.basic.constants.api.regions.LeagueShard) validated by LolApiParameters.requiredShard (src/main/java/com/safjnest/spring/controller/LolApiParameters.java:163) on platform-scoped endpoints. Search is global by default; its optional shard query parameter limits results to one active shard. Active values from LeagueShardUtils.getActives (src/main/java/com/safjnest/lol/utils/LeagueShardUtils.java:64): EUW1, NA1, KR, EUN1, JP1, BR1, LA1, LA2, TR1, RU, OC1, VN2, SG2, TW2, ME1. No authentication — protection only via validation and RiotScheduler.
curl "https://api.safjnest.com/api/lol/search?q=Faker%23EUW"
curl "https://api.safjnest.com/api/lol/search?q=Faker%23EUW&shard=EUW1"
curl "https://api.safjnest.com/api/lol/EUW1/profile/abc-puuid"
curl "https://api.safjnest.com/api/lol/leaderboard?queue=RANKED_SOLO_5X5&page=1&limit=20"
curl "https://api.safjnest.com/api/status"
Envelope — ApiResult and HTTP mapping
Services return ApiResult<T> (src/main/java/com/safjnest/lol/model/ApiResult.java:3):
public record ApiResult<T>(Status status, T payload, ResponseMetadata metadata) {
public enum Status { READY, PARTIAL, PENDING, NOT_FOUND }
}
LolApiResponses
LolApiResponses.from (src/main/java/com/safjnest/spring/controller/LolApiResponses.java:14) maps ApiResult → HTTP:
return switch (result.status()) {
case READY, PARTIAL -> ResponseEntity.ok(result.payload());
case PENDING -> ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new LolApiError(HttpStatus.ACCEPTED.value(), pendingCode, pendingMessage, result.metadata()));
case NOT_FOUND -> throw new ResponseStatusException(HttpStatus.NOT_FOUND, notFoundMessage);
};
ApiResult.Status | HTTP | Body | When |
|---|---|---|---|
READY | 200 OK | direct payload | complete and fresh data |
PARTIAL | 200 OK | payload + metadata.refresh=true | ProfileService.get stale (src/main/java/com/safjnest/lol/service/ProfileService.java:58) — client shows data but knows a refresh is in progress |
PENDING | 202 ACCEPTED | LolApiError with pending code + metadata | work queued via ComputeScheduler / SyncScheduler |
NOT_FOUND | 404 NOT_FOUND | LolApiError not_found | puuid/match does not exist |
POST /profile/{puuid}/refresh is the only one returning 204 No Content on success (LolController.java:88).
LolApiError — 202 and 4xx errors
LolApiError (src/main/java/com/safjnest/spring/dto/LolApiError.java:5) — sole Spring DTO for errors (ADR-0005/0007):
public record LolApiError(int status, String code, String message, ResponseMetadata metadata) {}
Example 202:
{
"status": 202,
"code": "profile_pending",
"message": "Profile initialization is pending",
"metadata": { "lastUpdate": null, "refresh": true, "filter": { "...": "..." } }
}
GET /profile/{puuid} adds header X-Profile-Refresh: true when metadata.refresh is true (LolController.java:294).
Pending code catalog (see reference/errors.mdx):
profile_pending, match_pending, leaderboard_pending, champion_pending, champion_tier_list_pending, champion_data_pending, profile_activity_pending, profile_matchups_pending, profile_records_pending.
ResponseMetadata — pagination / lastUpdate / refresh / filter
ResponseMetadata (src/main/java/com/safjnest/lol/model/ResponseMetadata.java:3):
public record ResponseMetadata(Pagination pagination, Long lastUpdate, Boolean refresh, Filter filter) {
public record Pagination(Integer page, Integer pageSize, Integer limit, Integer offset, Long total, Long pages, Boolean hasMore) {}
}
pagination—page/pageSizefor leaderboard (LeaderboardController.java:46),limit/offset/total/hasMorefor matches (LolController.java:123),pagesfor leaderboard.lastUpdate— epoch millis of last update (null if pending).refresh—true→ client should re-check; also used as headerX-Profile-Refresh.filter—Filterthat generated the data (echo for debugging).
LolApiParameters — centralized validation
Every parameter goes through LolApiParameters (src/main/java/com/safjnest/spring/controller/LolApiParameters.java:27). Errors → ResponseStatusException(400) via invalid (:235), mapped by LolApiExceptionHandler.
| Method | File:line | Rule |
|---|---|---|
requiredShard / region | :163 / :155 | non-blank, valid LeagueShard, UNKNOWN rejected |
requiredText | :230 | trim, non-empty — puuid, gameName, tagLine, matchId |
rank / requiredRank | :37 / :42 | TierType enum optional/required |
queue / optionalQueue / activityQueue | :47 / :56 / :52 | GameQueueType, default TEAM_BUILDER_RANKED_SOLO (:29), ALL→null |
rankHistoryQueue | :61 | only RANKED_SOLO_5X5 / RANKED_FLEX_SR (canonical) |
role + validateRole | :168 / :177 | LaneType among TOP,JUNGLE,MID,BOT,UTILITY (LaneTypeUtils.playables), rejected if GameQueueTypeUtils.hasLane (:30) is false |
patch | :103 | regex \d+\.\d+ — major.minor |
page / limit | :198 / :203 | page>=1, leaderboard limit 1..50 |
matchLimit / matchOffset | :210 / :217 | limit 1..100, offset>=0 |
matchOrder | :222 | timeStart:asc or timeStart:desc (default desc) |
activityFilter | :146 | coherent start/end, champion>=0, factory Filter.canonical() or Filter.summoner() |
matchupsFilter | :124 | start/end, queue, patch, role, minGames ≥1 |
rankHistoryQuery | :70 | queue/view/season/patch/timeStart/timeEnd/sort mutually exclusive, validation via SeasonUtils |
otpChampion | :183 | integer >0 |
recordMetric | :193 | required RecordMetric enum |
minGames | :119 | >=1 |
LolApiResponses — complete mapping
Shown above. LolApiExceptionHandler (src/main/java/com/safjnest/spring/controller/LolApiExceptionHandler.java:23) translates:
| Exception | HTTP | code | File:line |
|---|---|---|---|
ResponseStatusException | .getStatusCode() | codeFor (:89): 400→invalid_request, 404→not_found, 405→method_not_allowed | :23 |
MissingServletRequestParameterException | 400 | invalid_request | :31 |
MissingPathVariableException | 400 | invalid_request | :40 |
MethodArgumentTypeMismatchException | 400 | invalid_request | :49 |
HttpRequestMethodNotSupportedException | 405 | method_not_allowed | :58 |
NoHandlerFoundException / NoResourceFoundException | 404 | not_found | :67 |
| Other | 500 | internal_error | :76 |
CORS *
LolApiConfig.addCorsMappings (src/main/java/com/safjnest/spring/config/LolApiConfig.java:42):
registry.addMapping("/api/lol/**").allowedOrigins("*").allowedMethods("GET","POST","OPTIONS").allowedHeaders("*");
registry.addMapping("/api/status").allowedOrigins("*").allowedMethods("GET","OPTIONS").allowedHeaders("*");
All browser clients can call without custom preflight.
Jackson MixIn
LolApiConfig.configureMessageConverters (src/main/java/com/safjnest/spring/config/LolApiConfig.java:27):
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.addMixIn(ChampionStatistics.class, FilterApiMixin.class);
mapper.addMixIn(Build.class, FilterApiMixin.class);
mapper.addMixIn(Filter.class, FilterApiVisibilityMixin.class);
converters.add(new MappingJackson2HttpMessageConverter(mapper));
FilterApiMixin(:36):@JsonIgnoreProperties("filter")onChampionStatisticsandBuild— the internalfilterfield is not serialized in response JSON.FilterApiVisibilityMixin(:39):@JsonAutoDetect(fieldVisibility = ANY)onFilter— ensures field serialization even whenFilteris included inResponseMetadata.
Shared codec for Mongo/Redis: JsonCodec (src/main/java/com/safjnest/utils/JsonCodec.java:17) with FIELD ANY, GETTER NONE, JavaTimeModule, WRITE_DATES_AS_TIMESTAMPS=false.
Pattern index — all routes
/api/lol/{shard} — LolController (LolController.java:36)
| # | Method | Pattern | File:line | Description |
|---|---|---|---|---|
| 2 | GET | /api/lol/{shard}/profile/{puuid} | :63 | full profile SummonerView (200/202/404) |
| 3 | GET | /api/lol/{shard}/profile-by-name/{gameName}/{tagLine} | :226 | profile by Riot ID |
| 4 | POST | /api/lol/{shard}/profile/{puuid}/refresh | :75 | request refresh — 204, cooldown 2m |
| 5 | GET | /api/lol/{shard}/profile/{puuid}/matches | :91 | paginated match history |
| 6 | GET | /api/lol/{shard}/profile/{puuid}/rank-history | :130 | rank progress history |
| 7 | GET | /api/lol/{shard}/profile/{puuid}/activity | :156 | aggregated activity (202 if computing) |
| 8 | GET | /api/lol/{shard}/profile/{puuid}/matchups | :184 | matchups by opponent |
| 9 | GET | /api/lol/{shard}/profile/{puuid}/records | :209 | profile records |
| 10 | GET | /api/lol/{shard}/livegame/{puuid} | :240 | live game by puuid |
| 11 | GET | /api/lol/{shard}/livegame-by-name/{gameName}/{tagLine} | :253 | live game by Riot ID |
| 12 | GET | /api/lol/{shard}/match/{gameId} | :268 | match detail (202 if syncing) |
/api/lol — global
| # | Method | Pattern | Controller:line |
|---|---|---|---|
| 1 | GET | /api/lol/search?q=&shard= | LolController.java:47 |
| 13 | GET | /api/lol/leaderboard | LeaderboardController.java:24 |
| 14 | GET | /api/lol/leaderboard/rank-distribution | :60 |
| 15 | GET | /api/lol/leaderboard/top-regions | :71 |
| 16 | GET | /api/lol/champion/indexables | ChampionController.java:25 |
| 17 | GET | /api/lol/champions/tier-list | :30 |
| 18 | GET | /api/lol/champion/{champion} | :51 |
| 19 | GET | /api/lol/records | RecordsController.java:25 |
| 20 | GET | /api/lol/records/{metric} | :32 |
| 21 | GET | /api/lol/profile/indexables | ProfileIndexableController.java:18 |
| 22 | GET | /api/lol/ai/training | AiTrainingController.java:23 |
/api — system
| # | Method | Pattern | Controller:line |
|---|---|---|---|
| 23 | GET | /api/status | StatusController.java:20 |
Note: the spec says "17 patterns" — the table actually lists 23. The 17 canonical documents are
1–12+13–17; the remaining (records,profile/indexables,ai/training,status,livegame-by-name,profile/records) bring the total to 23.
Quick examples
# Search across every shard, or limit it with `shard`
curl "https://api.safjnest.com/api/lol/search?q=Hide%20on%20bush%23KR1"
curl "https://api.safjnest.com/api/lol/search?q=Hide%20on%20bush%23KR1&shard=EUW1"
# Profile (handle 202)
curl -i "https://api.safjnest.com/api/lol/EUW1/profile/abc-123-puuid"
# Leaderboard
curl "https://api.safjnest.com/api/lol/leaderboard?queue=RANKED_SOLO_5X5&rank=MASTER&page=1&limit=20"
# Champion tier list
curl "https://api.safjnest.com/api/lol/champions/tier-list?patch=15.6&rank=EMERALD®ion=EUW1"
# Records
curl "https://api.safjnest.com/api/lol/records/KILLS?region=EUW1&limit=20&offset=0"
# Status
curl "https://api.safjnest.com/api/status"
