Skip to content

API Interface Design

This document is based on the following source files:

  • internal/app/routers.go -- complete route registration (Chi v5 routing tree)
  • internal/handlers/response.go -- response helper functions (respondJSON / respondError)
  • internal/handlers/*.go -- the various business Handler definitions and Swagger annotations
  • internal/middleware/auth.go -- JWT authentication middleware (Bearer + query param)
  • internal/jsplugin/routes.go -- JS plugin static/API route registration
  • docs/api_response.md -- API response format specification
  • AGENTS.md -- API documentation conventions + config interface conventions (hard rules)

Table of Contents

  1. Design Principles
  2. Response Format Specification
  3. Route Organization Structure
  4. Authentication Mechanism
  5. Handler Creation Pattern
  6. Comparison of the Three Config Interfaces
  7. Swagger Documentation Specification
  8. Complete Route List

1. Design Principles

Section sources: docs/api_response.md, AGENTS.md (backend coding conventions)

The Songloft backend API follows these core principles:

  • RESTful direct return: successful responses return the business model or collection directly, without a unified {code, data, message} envelope. The HTTP status code carries the semantics; a code field duplicating it is redundant.
  • Unified error format: error responses from all JSON API endpoints are uniformly {"error": "...", "detail": "..."}; using alternative field names such as message, msg, or reason is forbidden.
  • Resource-oriented paths: URL paths use plural nouns to represent resource collections (/songs, /playlists), and HTTP methods express the operation semantics (GET to read, POST to create, PUT to modify, DELETE to delete).
  • Standardized pagination: list endpoints paginate via the limit + offset query parameters, and responses include total, limit, and offset metadata.
  • Binary stream exception: errors from binary-stream endpoints such as play (/songs/{id}/play), proxy (/proxy), and static files may use plaintext http.Error(), since the client does not expect a JSON body.

2. Response Format Specification

Section sources: internal/handlers/response.go, docs/api_response.md

2.1 Response Helper Functions

All handlers output success responses via respondJSON(w, status, data) (data is serialized directly as top-level JSON) and output errors via respondError(w, status, message, err) (which automatically builds {"error","detail"}). The middleware layer uses a separate respondAuthError with a consistent format.

2.2 Three Forms of Success Response

ScenarioFormatExample
Single entityModel serialized directly{"id":1, "title":"Track", ...}
Paginated listCollection name + pagination metadata{"songs":[...], "total":100, "limit":20, "offset":0}
Operation resultMessage string{"message": "Song deleted"}

2.3 Error Response Format

json
{
  "error": "Human-readable error message",
  "detail": "Optional low-level technical detail"
}
  • error -- always present, a short user-facing description
  • detail -- optional, output only when err != nil, containing the low-level error information

Corresponds to the models.ErrorResponse struct; custom field names are forbidden.


3. Route Organization Structure

Section sources: internal/app/routers.go, internal/jsplugin/routes.go

3.1 Routing Tree Overview

Route registration is split into three layers, orchestrated uniformly by App.setupRouter():

chi.Router (root)
├── Global middleware: Compress → Logger → Recoverer → RequestID → CORS
├── Frontend static files / Swagger (dev build)
├── /api/v1 ─┬─ [Public] /auth/login, /auth/refresh, /version, /health
│            └─ [Bearer] /auth/*, /songs/*, /playlists/*, /settings/*,
│                        /configs/*, /scan/*, /cache-manage/*, /upgrade/*, /proxy
├── /api/v1/jsplugins/* [Bearer] plugin CRUD + registry + /plugins/health
├── /api/v1/jsplugin/{entryPath}[/static/*] [Public] plugin static assets
├── /api/v1/jsplugin-assets/* [Public] common CSS/JS/fonts
└── /api/v1/jsplugin/{entryPath}/* [Bearer+PublicPathChecker] API forwarding

3.2 Middleware Stack

Global middleware is registered in setupBaseRouter(), in execution order: Compress(Gzip) -> RequestLogger(slog) -> Tracely(panic reporting) -> Recoverer(500) -> RequestID -> CORS. The authentication middleware AuthMiddleware is not global middleware; it is added on demand inside each route group.

3.3 Route Grouping Strategy

Chi v5's r.Group() divides authentication boundaries: under the same /api/v1 prefix, public endpoints are registered directly, while authenticated endpoints are wrapped by r.Group + r.Use(AuthMiddleware). JS plugin routes are registered separately; static assets require no authentication, and API forwarding supports the PublicPathChecker interface to exempt plugin-declared public paths.


4. Authentication Mechanism

Section sources: internal/middleware/auth.go

4.1 JWT Dual-Token Mechanism

The system uses JWT dual-token authentication:

  • Access Token -- short-lived, used for API request authentication
  • Refresh Token -- long-lived, used to refresh the access token

4.2 Token Passing Methods

The authentication middleware tries two methods, in priority order, to obtain the token:

  1. Authorization Header (priority): Authorization: Bearer <token>
  2. Query Parameter (fallback): ?access_token=<token>

The query parameter fallback mainly serves scenarios where a custom header cannot be set: <img> tags loading cover art, <audio> tags loading audio, CachedNetworkImage, etc.

4.3 Public Endpoints

The following endpoints require no authentication and are registered directly outside the AuthMiddleware:

EndpointPurpose
POST /api/v1/auth/loginUser login
POST /api/v1/auth/refreshRefresh token
GET /api/v1/versionVersion information
GET /api/v1/healthHealth check
GET /api/v1/jsplugin/{entryPath}Plugin static page
GET /api/v1/jsplugin/{entryPath}/static/*Plugin static assets
GET /api/v1/jsplugin-assets/*Plugin common assets

In addition, API paths declared by a plugin via publicPaths in its manifest are exempted by the PublicPathChecker interface inside the authentication middleware.


5. Handler Creation Pattern

Section sources: internal/handlers/*.go, internal/app/routers.go

5.1 Factory Function Pattern

Each Handler follows a three-step creation: (1) a struct holding service dependencies; (2) a NewXxxHandler(...) factory function that receives services and returns a pointer; (3) an optional SetXxx(fn) setter to resolve circular dependencies or deferred binding. Taking SongHandler as an example, the factory function receives 6 dependencies such as SongService, CacheService, and AsyncReassigner, and after creation injects the Scanner's path-getter function via SetGetMusicPath.

5.2 Handler List

The project has 14 Handlers in total, all located in internal/handlers/:

  • Business core: AuthHandler(AuthService), SongHandler(SongService + CacheService + 4 dependencies), PlaylistHandler(PlaylistService), BackupHandler(BackupService)
  • Config management: ConfigHandler(ConfigService), ScanHandler(SongService + Scanner + ConfigService), HLSHandler(SongService + ConfigService), CacheHandler(CacheService + ConfigService), LogHandler(ConfigService + LevelVar)
  • Plugin/upgrade: JSPluginHandler(PackageManager + Repository + Manager + SourceMetrics + ConfigService), UpgradeHandler(UpgradeService)
  • Utility: ProxyHandler(no dependencies), VersionHandler(no dependencies), HealthHandler(no dependencies)

5.3 Callback Injection

Some handlers bind cross-module callbacks via setters: configHandler.SetOnConfigChanged triggers side effects after a generic KV write, scanHandler.SetOnMusicPathChanged / SetOnAutoScanChanged bind rebuild logic after config changes, and songHandler.SetGetMusicPath lazily injects the Scanner's path function.


6. Comparison of the Three Config Interfaces

Section sources: AGENTS.md (config interface conventions, hard rules), internal/app/routers.go

Three config interface styles exist in the project, each with a clear division of labor:

Dimension/settings/<name>/<module>/config/configs/{key}
PositioningBusiness feature toggle (user-facing)Module aggregate configadmin generic KV editing
Path style/settings/<kebab-case>/<module>/config/configs/{key}
Data formStrongly typed JSONStrongly typed JSON{key, value} string
Default valueHandled internally by the handlerHandled internally by the handlerNone (returns 404 if the key does not exist)
Side effectsTriggered directly inside PUTTriggered directly inside PUTRequires an onConfigChanged callback
OwnershipCorresponding business module handlerModule handlerConfigHandler
Use caseIsolated config or cross-module sharingStrongly related to module action endpointsadmin debugging / hand-editing

6.1 Business Endpoints /settings/<name>

Currently 17 GET/PUT endpoint pairs, distributed across SongHandler (remote-title-source), HLSHandler (hls-proxy), ScanHandler (music-path, scan-playlist-mode, scan-auto-create-playlists, scan-title-source, auto-scan -- 5 scan-related), LogHandler (log-level), JSPluginHandler (plugin-registries, http-proxy, plugin-keep-alive, plugin-auto-update), UpgradeHandler (github-proxy), and ConfigHandler (tab-config, library-browse, user-preferences, equalizer). The data is all strongly typed JSON, such as {enabled: bool}, {proxy: string}, etc., with the handler handling defaults and side effects internally.

6.2 Module Aggregate Endpoints

A typical example is cache management /cache-manage/*, where config (GET/PUT) shares the prefix and CacheService with stats, clean, and validate-dir. Applicable to scenarios where config is strongly related to module action endpoints.

6.3 Generic KV /configs/{key}

Used only by the frontend generic config editor (admin hand-editing); no strong typing, no side effects (unless an onConfigChanged callback is attached), and PUT returns 404 when the key does not exist. New business features are forbidden from calling it directly.

6.4 Dual-Entry Consistency

Some config is modified by both a business endpoint and generic KV (such as music_path). The musicPathChanged closure in routers.go ensures both entries share the same side-effect function -- the business endpoint triggers it directly inside the PUT handler, and generic KV triggers it via the onConfigChanged callback.


7. Swagger Documentation Specification

Section sources: AGENTS.md (API documentation conventions, hard rules)

7.1 Hard Rules

Every handler method registered in routers.go must have swag annotations. No exemptions.

7.2 Required Fields

Each handler contains at least the following 7 swag annotations:

FieldDescription
@SummaryA one-line summary in Chinese
@DescriptionDetailed description (side effects/defaults/error-code triggers)
@TagsBusiness grouping (in Chinese), reusing existing tags
@ProduceResponse format (usually json)
@SuccessSuccess response type and description
@SecurityBearerAuth (omitted for public endpoints)
@RouterPath and method

Endpoints with a request body additionally add @Accept json and @Param request body.

7.3 Existing Business Tags

歌曲管理 | 歌单管理 | 电台与 HLS | 扫描管理 | 配置管理
缓存管理 | JS插件管理 | JS 插件 | 数据备份 | 设置
升级 | 认证

Creating new tags on a whim is forbidden.

7.4 Multi-Alias Routes and Validation

  • Multiple alias paths (such as /songs/{id}/play and /songs/{id}/play.m3u8) each get their own @Router line; HEAD is not listed separately
  • Catch-all routes list all actual methods; dynamic routes note their placeholder nature in @Description
  • After modifying annotations you must run make swagger to regenerate; the artifacts (docs/swagger.json, docs/swagger.yaml, docs/docs.go) must be committed
  • Validation: output contains the new @Router path + grep hits in swagger.json + visual inspection at /swagger/index.html after startup

8. Complete Route List

Diagram sources: internal/app/routers.go, internal/handlers/jsplugin.go, internal/jsplugin/routes.go

The following route list covers all endpoints registered in routers.go, JSPluginHandler.RegisterRoutes, and jsplugin/routes.go. Unless otherwise noted, all require Bearer authentication.

8.1 Authentication (AuthHandler)

MethodPathAuthDescription
POST/auth/loginNoneUser login
POST/auth/refreshNoneRefresh token
POST/auth/logoutBearerLogout
GET/auth/tokensBearerList all tokens
GET/auth/tokens/{token_id}BearerToken details
DELETE/auth/tokens/{token_id}BearerRevoke token

8.2 Songs (SongHandler + HLSHandler)

MethodPathDescription
GET/songsSong list (pagination + filtering)
GET/songs/idsSong ID list
POST/songs/remoteAdd remote song
POST/songs/radioAdd radio
POST/songs/cleanClean up invalid songs
POST/songs/batch-deleteBatch delete
POST/songs/organizeOrganize song files
POST/songs/organize/previewPreview batch organize (dry-run)
GET/songs/duplicatesDuplicate song detection
GET/songs/facetsTag category aggregation
POST/songs/refresh-metadataStart remote metadata refresh
GET/songs/refresh-metadata/progressMetadata refresh progress
POST/songs/refresh-metadata/cancelCancel metadata refresh
GET/songs/{id}Get song details
PUT/songs/{id}Update song information
DELETE/songs/{id}Delete song
PUT/songs/{id}/lyricsUpdate lyrics
PUT/songs/{id}/tagsWrite audio tags
POST/songs/{id}/activateActivate song
POST/songs/{id}/playedPlay event notification (broadcast to plugins)
GET/HEAD/songs/{id}/playPlay audio stream (binary)
GET/HEAD/songs/{id}/play.m3u8HLS radio alias (same handler)
GET/songs/{id}/coverSong cover art
GET/songs/{id}/lyricSong lyrics
GET/HEAD/songs/{id}/hls/playlistHLS playlist proxy
GET/HEAD/songs/{id}/hls/segmentHLS segment proxy

8.3 Playlists (PlaylistHandler + BackupHandler)

MethodPathDescription
GET/playlists/exportExport playlists
POST/playlists/importImport playlists
GET/playlistsPlaylist list
POST/playlistsCreate playlist
PUT/playlists/reorderReorder playlists
GET/playlists/{id}Playlist details
PUT/playlists/{id}Update playlist
DELETE/playlists/{id}Delete playlist
POST/playlists/batch-deleteBatch delete playlists
GET/playlists/{id}/songsSongs in a playlist
POST/playlists/{id}/songsAdd song to playlist
PUT/playlists/{id}/songs/reorderReorder songs in a playlist
DELETE/playlists/{id}/songs/{songId}Remove a song from a playlist
POST/playlists/{id}/touchUpdate playlist access time
POST/playlists/{id}/coverUpload playlist cover art
GET/playlists/{id}/coverGet playlist cover art

8.4 Config and Settings

MethodPathHandlerDescription
GET/PUT/settings/remote-title-sourceSongHandlerNetwork song title source
GET/PUT/settings/hls-proxyHLSHandlerHLS proxy toggle
GET/PUT/settings/music-pathScanHandlerMusic library path
GET/PUT/settings/scan-playlist-modeScanHandlerPlaylist merge mode
GET/PUT/settings/scan-auto-create-playlistsScanHandlerAuto-create playlists
GET/PUT/settings/scan-title-sourceScanHandlerTitle source
GET/PUT/settings/auto-scanScanHandlerAuto scan
GET/PUT/settings/log-levelLogHandlerLog level
GET/PUT/settings/plugin-registriesJSPluginHandlerPlugin registries
GET/PUT/settings/http-proxyJSPluginHandlerHTTP proxy
GET/PUT/settings/plugin-keep-aliveJSPluginHandlerPlugin keep-alive allowlist
GET/PUT/settings/plugin-auto-updateJSPluginHandlerPlugin auto-update
GET/PUT/settings/github-proxyUpgradeHandlerGitHub update proxy
GET/PUT/settings/tab-configConfigHandlerTab page config
GET/PUT/settings/library-browseConfigHandlerLibrary browse view
GET/PUT/settings/user-preferencesConfigHandlerUser preferences
GET/PUT/settings/equalizerConfigHandlerEqualizer
GET/configsConfigHandlerConfig list (generic KV)
POST/configsConfigHandlerCreate config
GET/configs/{key}ConfigHandlerGet config
PUT/configs/{key}ConfigHandlerUpdate config
DELETE/configs/{key}ConfigHandlerDelete config

8.5 Scan (ScanHandler)

MethodPathDescription
POST/scanScan and import
GET/scan/progressScan progress
POST/scan/cancelCancel scan
GET/scan/directoriesDirectory list
GET/scan/dir-namesDirectory name list
GET/scan/fingerprints/statusFingerprint status
POST/scan/fingerprintsStart fingerprint computation
GET/scan/fingerprints/progressFingerprint computation progress

8.6 Cache (CacheHandler)

MethodPathDescription
GET/cache-manage/statsCache statistics
POST/cache-manage/cleanClean cache
GET/cache-manage/configCache config
PUT/cache-manage/configUpdate cache config
POST/cache-manage/validate-dirValidate cache directory

8.7 Upgrade (UpgradeHandler)

MethodPathDescription
GET/upgrade/versionsVersion list
GET/upgrade/checkCheck for updates (Docker only)
POST/upgrade/startStart upgrade
POST/upgrade/resetReset to base image
GET/upgrade/progressUpgrade progress

8.8 JS Plugin Management (JSPluginHandler)

MethodPathDescription
GET/jspluginsPlugin list
POST/jsplugins/uploadUpload plugin
POST/jsplugins/update-allBatch update
POST/jsplugins/storage/cleanupClean up orphaned persistent storage
POST/jsplugins/registry/refreshRefresh registry
POST/jsplugins/registry/installInstall from registry
GET/jsplugins/{id}Plugin details
PUT/jsplugins/{id}Update plugin
DELETE/jsplugins/{id}Delete plugin
POST/jsplugins/{id}/enableEnable plugin
POST/jsplugins/{id}/disableDisable plugin
GET/jsplugins/{id}/check-updateCheck for plugin update
POST/jsplugins/{id}/updateDownload update
GET/plugins/healthAudio source health

8.9 JS Plugin Runtime (jsplugin.Manager)

MethodPathAuthDescription
GET/jsplugin/{entryPath}[/]NonePlugin entry HTML
GET/jsplugin/{entryPath}/static[/*]NonePlugin static assets
GET/jsplugin-assets/*NoneCommon CSS/JS/fonts
GET/HEAD/jsplugin/{entryPath}/files/*BearerPlugin file serving
ANY/jsplugin/{entryPath}/*Bearer*API catch-all forwarding

*Note: paths declared in the plugin manifest's publicPaths are exempted from authentication via PublicPathChecker.

8.10 Other

MethodPathAuthDescription
GET/versionNoneVersion information
GET/healthNoneHealth check
GET/proxyBearerExternal resource CORS proxy

All paths above omit the /api/v1 prefix. The full URL is http://<host>:58091/api/v1/<path>.