一键导入
api-endpoint-review-checklist
10-point checklist for reviewing new API endpoints in multi-user, dual-provider (PostgreSQL/SQLite) contexts
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
10-point checklist for reviewing new API endpoints in multi-user, dual-provider (PostgreSQL/SQLite) contexts
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
{what this skill teaches agents}
Use when working with an Aspire distributed application and operating the AppHost or its resources through the Aspire CLI: start/restart/stop/wait on the app; iterate via watch, rebuild, hot reload, or resource commands; inspect resources, logs, traces, docs, or health; add integrations; manage secrets/config; publish, deploy, or rerun a named pipeline step; initialize Aspire in an existing app; recover missing `.modules` files in a TypeScript AppHost; discover the frontend URL for Playwright from Aspire state; expose custom dashboard/resource commands; or understand Aspire AppHost APIs in C# or TypeScript. Use it even if the task is described in terms of AppHost, resources, dashboard, app bootstrap, missing generated modules, Playwright URL discovery, or local distributed app workflow without naming Aspire. Do not use for non-Aspire .NET apps, container-only repos with no AppHost, or ordinary build and test tasks.
Systematic recovery procedure for Aspire AppHost failures caused by orphaned processes holding critical ports (especially 22070). Covers diagnostics, two-pass cleanup (AppHost + dcp tree, then orphaned services), verification, and restart validation. USE FOR: "aspire won't start", "cannot access disposed object", "address already in use", "aspire dashboard not loading", "port 22070 in use", "aspire restart failed", "orphaned dcp processes", dashboard stuck on "starting", build succeeds but services won't start, previous Aspire session crashed and won't restart. DO NOT USE FOR: initial Aspire setup, configuration changes, deployment issues, or general Aspire CLI usage (use the aspire skill instead).
End-to-end testing and verification for SentenceStudio. USE THIS SKILL whenever the user says "test", "verify", "check", "validate", "confirm it works", "smoke test", "run the app and check", "does it work", "try it", "make sure", or any variation of testing a feature or fix in a running app. Also use after EVERY bug fix or feature implementation as a mandatory final verification step — even if you think a build check is enough. Covers: launching via Aspire, interacting with Playwright (webapp) or maui-devflow-debug (native), verifying UI state, checking database records, and reading structured logs. If someone asks you to test anything in this app, or to verify a fix works, or to run a smoke test, or to check that CRUD operations work, or to confirm audio/quiz/import/activity features behave correctly — this is the skill to use. Do NOT skip this skill when verification is needed.
Run build, deploy, inspect, and fix loops for .NET MAUI apps that already have MAUI DevFlow integrated. USE FOR: launching MAUI apps, selecting devices or emulators, waiting for or recovering agent connections, broker/port/adb connectivity issues, visual tree inspection, screenshots, UI interaction, Blazor WebView CDP debugging, reading DevFlow logs, and iterative app debugging. DO NOT USE FOR: first-time DevFlow package setup (use maui-devflow-onboard), or generic desktop automation unrelated to MAUI. INVOKES: maui devflow CLI, dotnet CLI, Android adb/android tools, and Apple simctl tools.
Add MAUI DevFlow to a .NET MAUI project with agent package references, MauiProgram.cs registration, Blazor WebView support, GTK variants, Central Package Management guidance, and verification commands. USE FOR: first-time DevFlow setup, reviewing what files to edit, choosing DevFlow packages, or continuing after `maui devflow init` installs skills. DO NOT USE FOR: troubleshooting an already-integrated app that cannot connect, iterative app debugging, UI inspection, or generic MAUI build failures (use maui-devflow-debug). INVOKES: maui devflow CLI and dotnet CLI.
| name | api-endpoint-review-checklist |
| description | 10-point checklist for reviewing new API endpoints in multi-user, dual-provider (PostgreSQL/SQLite) contexts |
| domain | api-design, security, validation |
| confidence | low |
| source | observed |
When reviewing new API endpoints in this project, check for:
This checklist emerged from reviewing commit 398a7690 (Profile, Speech, Maintenance endpoints).
Every endpoint that accepts a resource ID (profile, resource, import, etc.) MUST verify the authenticated user owns that resource BEFORE operating on it.
Pattern:
var userProfileId = user.FindFirstValue("user_profile_id");
if (string.IsNullOrEmpty(userProfileId))
return Results.Unauthorized();
// Check ownership BEFORE fetching resource
if (!string.Equals(userProfileId, profileId, StringComparison.Ordinal))
return Results.Forbid();
var resource = await db.Resources.FirstOrDefaultAsync(r => r.Id == resourceId);
Anti-pattern:
// ❌ WRONG — fetches resource first, THEN checks ownership
var resource = await repository.GetByIdAsync(resourceId);
if (resource.UserProfileId != userProfileId)
return Results.Forbid();
Why order matters: The ownership check is cheap (string comparison). If you fetch the resource first, you've already done expensive DB work for a request you're going to reject.
NEVER use repository.ListAsync().FirstOrDefault(predicate) in API endpoints.
Why: Fetches ALL rows from the table into memory, then filters client-side. Performance bomb at scale.
Detection:
grep -r "\.ListAsync().*\.FirstOrDefault" src/SentenceStudio.Api
Fix: Use direct DB query scoped by ID:
// ❌ WRONG
var profile = (await repository.ListAsync()).FirstOrDefault(p => p.Id == profileId);
// ✅ CORRECT
var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.Id == profileId);
All request bodies MUST be validated. Use TypedResults.ValidationProblem for structured error responses.
Pattern:
if (string.IsNullOrWhiteSpace(request.DisplayName))
return TypedResults.ValidationProblem(new Dictionary<string, string[]> {
{ nameof(request.DisplayName), new[] { "Display name is required" } }
});
if (request.PreferredSessionMinutes < 1 || request.PreferredSessionMinutes > 480)
return TypedResults.ValidationProblem(new Dictionary<string, string[]> {
{ nameof(request.PreferredSessionMinutes), new[] { "Session minutes must be between 1 and 480" } }
});
Anti-pattern:
// ❌ WRONG — commit message claims validation, code has none
// Accepts empty strings, negative numbers, malformed emails
Existing precedent: FeedbackEndpoints.cs:50-54 validates length + emptiness (uses BadRequest strings, not ideal, but better than nothing).
All async endpoint handlers MUST accept CancellationToken cancellationToken and pass it to repository/service calls.
Pattern:
private static async Task<IResult> GetProfile(
string profileId,
ClaimsPrincipal user,
[FromServices] UserProfileRepository repository,
CancellationToken cancellationToken) // ← Required
{
var profile = await repository.GetByIdAsync(profileId, cancellationToken);
return Results.Ok(MapToDto(profile));
}
Why: Without cancellation support, if the client drops the connection, the server keeps running the query. Wastes resources.
Existing precedent: FeedbackEndpoints.cs:42, ChannelEndpoints.cs, ImportEndpoints.cs all use CancellationToken.
All endpoints SHOULD log at key decision points (success, failure, ownership rejection).
Pattern:
private readonly ILogger<ProfileEndpoints> _logger;
// Constructor injection
public ProfileEndpoints(ILogger<ProfileEndpoints> logger) => _logger = logger;
// Usage
_logger.LogInformation("User {UserId} updated profile {ProfileId}", userProfileId, profileId);
_logger.LogWarning("Profile {ProfileId} not found for user {UserId}", profileId, userProfileId);
Existing precedent: FeedbackEndpoints.cs:44 injects ILoggerFactory.
Use structured TypedResults.Problem instead of plain Results.Problem strings.
Pattern:
if (saved < 0)
{
_logger.LogError("Profile save failed for user {UserId}", profileId);
return TypedResults.Problem(
title: "Save failed",
detail: "Unable to save profile changes. Please try again.",
statusCode: 500
);
}
Why: Provides consistent error shape for clients (title, detail, status, type).
Endpoints that operate on ALL users (e.g., maintenance/migration tasks) MUST have explicit admin-only authorization OR per-user filtering.
Anti-pattern:
// ❌ WRONG — MaintenanceEndpoints.cs:24-34
var userProfileId = user.FindFirstValue("user_profile_id"); // Extracted but NOT used
var migrated = await progressService.MigrateToStreakBasedScoringAsync(); // Operates on ALL users!
Fix (per-user):
var migrated = await progressService.MigrateToStreakBasedScoringAsync(userProfileId);
Fix (admin-only):
group.MapPost("/migrate-streak", MigrateStreak)
.RequireAuthorization("AdminOnly"); // Add policy
For endpoints supporting mobile clients (Flutter, MAUI), response shapes MUST be stable across versions.
Pattern:
dynamic or object)string? vs string)// ElevenLabsApiKey: Accepted in PUT but not yet persisted (always null in GET).
// Will be added to UserProfile schema in future migration.
ElevenLabsApiKey: null
All data modifications (POST/PUT/DELETE) MUST work correctly in BOTH PostgreSQL (API) and SQLite (mobile sync).
Check:
"PascalCase", SQLite is case-insensitive)Existing pattern: See .squad/skills/ef-dual-provider-migrations/SKILL.md.
Endpoints SHOULD follow existing route conventions:
/api/v1/{resource} (e.g., /api/channels)/api/v1/{resource}/{id} (e.g., /api/v1/profile/{profileId})/api/v1/{resource}/api/v1/{resource}/{id}/api/v1/{resource}/{id}Anti-pattern:
// MaintenanceEndpoints.cs — scoped to /api/v1/vocabulary/progress
// But it's a debug-only admin operation, not a vocabulary API surface
var group = app.MapGroup("/api/v1/vocabulary/progress").RequireAuthorization();
Better: /api/v1/maintenance or /api/v1/admin/maintenance.
Good endpoint (ChannelEndpoints.cs:22-32):
private static async Task<IResult> GetChannels(
ClaimsPrincipal user,
[FromServices] ChannelMonitorService channelService)
{
var userProfileId = user.FindFirstValue("user_profile_id");
if (string.IsNullOrEmpty(userProfileId))
return Results.Unauthorized();
var channels = await channelService.GetAllAsync(userProfileId); // ← Scoped by userId
return Results.Ok(channels);
}
Bad endpoint (ProfileEndpoints.cs:50-61):
private static async Task<IResult> GetProfile(
string profileId,
ClaimsPrincipal user,
[FromServices] UserProfileRepository repository)
{
var ownership = ResolveOwnership(profileId, user);
if (ownership is not null) return ownership;
var profile = (await repository.ListAsync()).FirstOrDefault(p => p.Id == profileId); // ← Fetches ALL profiles!
if (profile is null) return Results.NotFound();
return Results.Ok(MapToDto(profile));
}
repository.ListAsync().FirstOrDefault(predicate)SaveAsync returns int, endpoint returns unsaved entity# Find fetch-all anti-pattern
grep -r "\.ListAsync().*\.FirstOrDefault" src/SentenceStudio.Api
# Find endpoints without CancellationToken
grep -l "private static async Task<IResult>" src/SentenceStudio.Api/*.cs | \
xargs grep -L "CancellationToken"
# Find endpoints without logging
grep -l "private static async Task<IResult>" src/SentenceStudio.Api/*.cs | \
xargs grep -L "ILogger"
.squad/skills/ef-dual-provider-migrations/SKILL.md — PostgreSQL/SQLite migration patterns.squad/decisions/inbox/wash-fetch-all-antipattern.md — Decision record for this patternThis is the first observation of these patterns. Confidence will increase after: