Skip to content
22 changes: 22 additions & 0 deletions app/api/research/albums/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchAlbumsHandler } from "@/lib/research/getResearchAlbumsHandler";

/**
* OPTIONS /api/research/albums — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/albums — Artist album discography with release dates. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON album list or error
*/
export async function GET(request: NextRequest) {
return getResearchAlbumsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/audience/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchAudienceHandler } from "@/lib/research/getResearchAudienceHandler";

/**
* OPTIONS /api/research/audience — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/audience — Audience demographics by platform. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON audience demographics or error
*/
export async function GET(request: NextRequest) {
return getResearchAudienceHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/career/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchCareerHandler } from "@/lib/research/getResearchCareerHandler";

/**
* OPTIONS /api/research/career — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/career — Artist career history and milestones. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON career timeline or error
*/
export async function GET(request: NextRequest) {
return getResearchCareerHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/charts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchChartsHandler } from "@/lib/research/getResearchChartsHandler";

/**
* OPTIONS /api/research/charts — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/charts — Global chart positions by platform and country. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON chart positions or error
*/
export async function GET(request: NextRequest) {
return getResearchChartsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/cities/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchCitiesHandler } from "@/lib/research/getResearchCitiesHandler";

/**
* OPTIONS /api/research/cities — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/cities — Geographic listening data for an artist. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON city-level listener data or error
*/
export async function GET(request: NextRequest) {
return getResearchCitiesHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/curator/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchCuratorHandler } from "@/lib/research/getResearchCuratorHandler";

/**
* OPTIONS /api/research/curator — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/curator — Playlist curator details. Requires `?platform=` and `?id=` query params.
*
* @param request - must include `platform` and `id` query params
* @returns JSON curator profile or error
*/
export async function GET(request: NextRequest) {
return getResearchCuratorHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/deep/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { postResearchDeepHandler } from "@/lib/research/postResearchDeepHandler";

/**
* OPTIONS /api/research/deep — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* POST /api/research/deep — Deep, comprehensive research with citations. Body: `{ query }`.
*
* @param request - JSON body with `query` string
* @returns JSON research report with citations or error
*/
export async function POST(request: NextRequest) {
return postResearchDeepHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/discover/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchDiscoverHandler } from "@/lib/research/getResearchDiscoverHandler";

/**
* OPTIONS /api/research/discover — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/discover — Discover artists by genre, country, and growth criteria. Supports `?genre=`, `?country=`, `?sort=`, `?limit=` filters.
*
* @param request - filter criteria via query params
* @returns JSON array of matching artists or error
*/
export async function GET(request: NextRequest) {
return getResearchDiscoverHandler(request);
}
Comment on lines +1 to +22
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate route and handler symbols
fd --type f 'route.ts' app/api/research | sort
rg -n 'getResearchDiscoverHandler|/api/research/discover'

# Find tests referencing discover endpoint/handler
rg -n --iglob '*{test,spec}*.{ts,tsx,js,jsx}' \
  '/api/research/discover|getResearchDiscoverHandler'

# Check for explicit error-path assertions (status codes/messages)
rg -n --iglob '*{test,spec}*.{ts,tsx,js,jsx}' \
  'discover.*(400|401|402|500)|status\s*[:=]\s*(400|401|402|500)|insufficient|invalid|missing'

Repository: recoupable/api

Length of output: 1160


🏁 Script executed:

cat -n lib/research/getResearchDiscoverHandler.ts

Repository: recoupable/api

Length of output: 2924


Add comprehensive test coverage and implement Zod query validation for this endpoint.

Missing test coverage for success and error paths (credit insufficiency, auth failure, proxy failures). Additionally, query parameters should be validated using a Zod schema (validateDiscoverQuery.ts) rather than manually parsed. The route file's JSDoc comments are empty and should document the endpoint's parameters and behavior.

The handler correctly uses validateAuthContext() and includes proper error handling, but these should be covered by tests. Create:

  • Tests covering auth success/failure, credit success/failure, and proxy success/failure paths
  • A lib/research/validateDiscoverQuery.ts file exporting a Zod schema and inferred type for country, genre, sort, limit, sp_monthly_listeners_min, sp_monthly_listeners_max parameters
  • JSDoc comments in the route file documenting the endpoint
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/research/discover/route.ts` around lines 1 - 18, Add Zod-based query
validation and comprehensive tests: create lib/research/validateDiscoverQuery.ts
exporting a Zod schema and inferred TypeScript type for the query params
(country, genre, sort, limit, sp_monthly_listeners_min,
sp_monthly_listeners_max) and update the discover handler to use that schema
instead of manual parsing (reference getResearchDiscoverHandler and the exported
schema name). Add JSDoc to the route file (the exported OPTIONS and GET
handlers) documenting accepted query parameters, auth requirements, and possible
responses. Add tests that exercise success and error paths for auth
(validateAuthContext), credit checks, and proxy failures by invoking the GET
route or directly calling getResearchDiscoverHandler with mocked dependencies,
and include tests validating the schema rejectsbad inputs and accepts valid
ones.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — JSDoc rewritten with meaningful one-liner descriptions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sidneyswift, thanks for updating the JSDoc! That addresses the documentation concern.

Two items from the original review are still outstanding:

  1. Zod-based query validation — query parameters (country, genre, sort, limit, sp_monthly_listeners_min, sp_monthly_listeners_max) are currently parsed manually in getResearchDiscoverHandler. Per the coding guidelines and the learning from AGENTS.md, all endpoints should use Zod for input parsing. A lib/research/validateDiscoverQuery.ts exporting a schema would make validation consistent and type-safe.

  2. Test coverage — the learning for this repo requires tests covering all success and error paths (auth failure, insufficient credits, proxy failure, and happy path). The discover endpoint currently has no tests.

Would you like to address these in this PR, or shall I open a follow-up issue to track them?


🧠 Learnings used
Learnt from: CR
Repo: recoupable/api PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T19:09:32.948Z
Learning: Applies to app/api/**/route.ts : Write tests for new API endpoints covering all success and error paths

Learnt from: CR
Repo: recoupable/api PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T19:09:32.948Z
Learning: Applies to app/api/**/route.ts : All API routes should have JSDoc comments

Learnt from: CR
Repo: recoupable/api PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T19:09:32.948Z
Learning: Applies to app/api/**/route.ts : All API endpoints should use a validate function for input parsing using Zod for schema validation

22 changes: 22 additions & 0 deletions app/api/research/enrich/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { postResearchEnrichHandler } from "@/lib/research/postResearchEnrichHandler";

/**
* OPTIONS /api/research/enrich — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* POST /api/research/enrich — Enrich an entity with structured web research data. Body: `{ url, prompt? }`.
*
* @param request - JSON body with `url` and optional `prompt`
* @returns JSON enriched entity data or error
*/
export async function POST(request: NextRequest) {
return postResearchEnrichHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/extract/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { postResearchExtractHandler } from "@/lib/research/postResearchExtractHandler";

/**
* OPTIONS /api/research/extract — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* POST /api/research/extract — Extract clean markdown from URLs. Body: `{ urls }`.
*
* @param request - JSON body with `urls` array
* @returns JSON extracted markdown content or error
*/
export async function POST(request: NextRequest) {
return postResearchExtractHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/festivals/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchFestivalsHandler } from "@/lib/research/getResearchFestivalsHandler";

/**
* OPTIONS /api/research/festivals — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/festivals — List of music festivals.
*
* @param request - optional filter query params
* @returns JSON festival list or error
*/
export async function GET(request: NextRequest) {
return getResearchFestivalsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/genres/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchGenresHandler } from "@/lib/research/getResearchGenresHandler";

/**
* OPTIONS /api/research/genres — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/genres — All available genre IDs and names.
*
* @param request - no required query params
* @returns JSON genre list or error
*/
export async function GET(request: NextRequest) {
return getResearchGenresHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/insights/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchInsightsHandler } from "@/lib/research/getResearchInsightsHandler";

/**
* OPTIONS /api/research/insights — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/insights — Noteworthy highlights and trending metrics for an artist. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON insights data or error
*/
export async function GET(request: NextRequest) {
return getResearchInsightsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/instagram-posts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchInstagramPostsHandler } from "@/lib/research/getResearchInstagramPostsHandler";

/**
* OPTIONS /api/research/instagram-posts — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/instagram-posts — Recent Instagram posts for an artist. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON Instagram posts or error
*/
export async function GET(request: NextRequest) {
return getResearchInstagramPostsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/lookup/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchLookupHandler } from "@/lib/research/getResearchLookupHandler";

/**
* OPTIONS /api/research/lookup — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/lookup — Resolve a Spotify artist URL to cross-platform IDs. Requires `?url=` query param.
*
* @param request - must include `url` query param (Spotify URL)
* @returns JSON cross-platform IDs or error
*/
export async function GET(request: NextRequest) {
return getResearchLookupHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/metrics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchMetricsHandler } from "@/lib/research/getResearchMetricsHandler";

/**
* OPTIONS /api/research/metrics — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/metrics — Platform-specific streaming and social metrics. Requires `?artist=` and `?source=` query params.
*
* @param request - must include `artist` and `source` query params
* @returns JSON metrics data or error
*/
export async function GET(request: NextRequest) {
return getResearchMetricsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/milestones/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchMilestonesHandler } from "@/lib/research/getResearchMilestonesHandler";

/**
* OPTIONS /api/research/milestones — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/milestones — Artist activity feed: playlist adds, chart entries, events. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON milestone activity feed or error
*/
export async function GET(request: NextRequest) {
return getResearchMilestonesHandler(request);
}
Loading
Loading