Problem
GET /streams/:id/tags is documented as an existing endpoint but does not exist. The shared type contract in packages/types/src/stream.ts tells callers:
/**
* ... May be `undefined` on endpoints that fetch a single stream
* directly (create / update / findOne) — callers that want the
* tags should hit `GET /streams/:id/tags`.
*/
tags?: Tag[]
The dashboard does exactly that: useStreamTags in app/hooks/useStreams.ts fetches `${NEXT_PUBLIC_API_URL}/streams/${id}/tags` with GET. But StreamTagsController (api/src/tags/tags.controller.ts) only defines POST /streams/:id/tags and DELETE /streams/:id/tags/:tagId — there is no GET handler. The request 404s, the tag editor on the streams dashboard falls into its error path, and per-stream tag chips can never load from this endpoint.
The tag data itself exists and is already returned inline on GET /streams (StreamsService.list batches tags via TagsService.listForStreamIds, issue #330), so the missing endpoint is purely a contract gap: the API, the shared types, and the app all describe a resource the server never serves.
Root cause
// api/src/tags/tags.controller.ts
@Controller("streams/:id/tags")
@UseGuards(StreamOwnershipGuard)
export class StreamTagsController {
@Post() ... // attach — exists
@Delete(":tagId") ... // detach — exists
// ← no @Get() handler
}
Why this is architecturally hard
- The repository layer already has the query material:
TagsDbRepository.listForStreamIds() (api/src/tags/repository/tags-db.repository.ts) loads tags grouped by stream in one round trip, and StreamsService.list uses it. A single-stream tags endpoint should reuse that path, but listForStreamIds is batch-oriented — the service needs a thin single-stream wrapper and a decision on the response envelope (paginated like every other list endpoint, or a plain array).
- Ownership semantics are already settled (
StreamOwnershipGuard on the controller), which makes this small; the genuinely open question is whether the endpoint returns PagedTags (matching app/lib/api/tags.ts's PagedTags shape) or the plain Tag[] that Stream.tags uses — the app's useStreamTags currently parses a PagedTags envelope, so the endpoint should match that or the hook must change.
- Contract coverage: the existing contract suite (
tests/contracts/src/streams.contract.ts) does not cover tag routes, so whatever shape is chosen should be pinned by a contract test on both the provider side (api/src/contract-provider.spec.ts) and the consumer side (xstreamroll-sdk/__tests__/contract.consumer.test.ts) to prevent a re-drift.
Acceptance criteria
Contract
Service
Tests
Documentation
Out of scope
Tag search/filtering on the global GET /tags list, and changing the inline tags field on GET /streams.
Getting started
Real files in scope: api/src/tags/tags.controller.ts, api/src/tags/tags.service.ts, api/src/tags/repository/tags-db.repository.ts, app/hooks/useStreams.ts (useStreamTags), packages/types/src/stream.ts, tests/contracts/src/streams.contract.ts.
Verify with:
cd api && npm run typecheck && npm test
cd ../xstreamroll-sdk && npm test
cd ../app && npm run typecheck && npm test
Good first files to read: api/src/tags/tags.controller.ts, app/hooks/useStreams.ts (useStreamTags), api/src/tags/repository/tags-db.repository.ts (listForStreamIds).
Problem
GET /streams/:id/tagsis documented as an existing endpoint but does not exist. The shared type contract inpackages/types/src/stream.tstells callers:The dashboard does exactly that:
useStreamTagsinapp/hooks/useStreams.tsfetches`${NEXT_PUBLIC_API_URL}/streams/${id}/tags`withGET. ButStreamTagsController(api/src/tags/tags.controller.ts) only definesPOST /streams/:id/tagsandDELETE /streams/:id/tags/:tagId— there is no GET handler. The request 404s, the tag editor on the streams dashboard falls into its error path, and per-stream tag chips can never load from this endpoint.The tag data itself exists and is already returned inline on
GET /streams(StreamsService.listbatches tags viaTagsService.listForStreamIds, issue #330), so the missing endpoint is purely a contract gap: the API, the shared types, and the app all describe a resource the server never serves.Root cause
Why this is architecturally hard
TagsDbRepository.listForStreamIds()(api/src/tags/repository/tags-db.repository.ts) loads tags grouped by stream in one round trip, andStreamsService.listuses it. A single-stream tags endpoint should reuse that path, butlistForStreamIdsis batch-oriented — the service needs a thin single-stream wrapper and a decision on the response envelope (paginated like every other list endpoint, or a plain array).StreamOwnershipGuardon the controller), which makes this small; the genuinely open question is whether the endpoint returnsPagedTags(matchingapp/lib/api/tags.ts'sPagedTagsshape) or the plainTag[]thatStream.tagsuses — the app'suseStreamTagscurrently parses aPagedTagsenvelope, so the endpoint should match that or the hook must change.tests/contracts/src/streams.contract.ts) does not cover tag routes, so whatever shape is chosen should be pinned by a contract test on both the provider side (api/src/contract-provider.spec.ts) and the consumer side (xstreamroll-sdk/__tests__/contract.consumer.test.ts) to prevent a re-drift.Acceptance criteria
Contract
GET /streams/:id/tagsexists underStreamOwnershipGuard, returns 200 for an owned stream and 403 for a non-owner (matching the existing guard semantics).app/hooks/useStreams.tsuseStreamTagsparses (PagedTags-shaped), or the hook is updated to the agreed shape — the two must agree after the change.packages/types/src/stream.tsaboutGET /streams/:id/tagsmatches reality.Service
TagsService.listForStreamIds/TagsDbRepository.listForStreamIds) rather than issuing a per-tag query.Tests
useStreamTags-driven test inapp(e.g.app/hooks/useStreams.test.tsxor the tag-editor tests) passes against the real endpoint shape.Documentation
Out of scope
Tag search/filtering on the global
GET /tagslist, and changing the inlinetagsfield onGET /streams.Getting started
Real files in scope:
api/src/tags/tags.controller.ts,api/src/tags/tags.service.ts,api/src/tags/repository/tags-db.repository.ts,app/hooks/useStreams.ts(useStreamTags),packages/types/src/stream.ts,tests/contracts/src/streams.contract.ts.Verify with:
Good first files to read:
api/src/tags/tags.controller.ts,app/hooks/useStreams.ts(useStreamTags),api/src/tags/repository/tags-db.repository.ts(listForStreamIds).