.cursor/skills/api/SKILL.md
Common patterns for the podverse-api Express application
npx skillsauth add podverse/podverse podverse-api-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides quick reference for common patterns used in the podverse-api application.
apps/api/packages/helpers*/): @podverse/helpers, @podverse/helpers-validation, @podverse/helpers-requests, @podverse/helpers-backend, @podverse/helpers-config@podverse/orm (from packages/orm/)@podverse/mq (from packages/mq/)@podverse/parser (from packages/parser/)| Package | Purpose |
| ----------------------------- | ------------------------------------------ |
| Helper packages | Types, DTOs, utilities, validation, config |
| @podverse/orm | Database entities and services |
| @podverse/mq | Message queue operations |
| @podverse/parser | Feed parsing |
| @podverse/external-services | Third-party service integrations |
| @podverse/notifications | Push notifications |
// apps/api/src/routes/podcast.ts
import { Router } from 'express';
import { PodcastController } from '../controllers/podcast';
import { asyncHandler } from '../lib/asyncHandler';
const router = Router();
router.get('/:id', asyncHandler(PodcastController.getById));
router.post('/', asyncHandler(PodcastController.create));
export default router;
// apps/api/src/controllers/podcast.ts
import { Request, Response } from 'express';
import { PodcastService } from '@podverse/orm';
export const PodcastController = {
async getById(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const podcast = await PodcastService.getById(id);
if (!podcast) {
res.status(404).json({ error: 'Podcast not found' });
return;
}
res.json(podcast);
} catch (error) {
console.error('Error getting podcast:', error);
res.status(500).json({ error: 'Internal server error' });
return;
}
},
async create(req: Request, res: Response): Promise<void> {
try {
const data = req.body;
const podcast = await PodcastService.create(data);
res.status(201).json(podcast);
} catch (error) {
console.error('Error creating podcast:', error);
res.status(500).json({ error: 'Internal server error' });
return;
}
},
};
Express route handlers should be typed as Promise<void> (or void for sync). Since res.json() returns a Response object, avoid using return with response methods.
Correct patterns:
// Early exit with status (use return for control flow)
if (!item) {
res.status(404).json({ error: 'Not found' });
return; // Return void, not Response
}
// Success response (no return needed)
res.json(data);
Incorrect pattern:
// DON'T do this - returns Response instead of void
return res.json(data);
return res.status(404).json({ error: 'Not found' });
The return keyword is acceptable for early exits when combined with response methods for single-line control flow:
if (!user) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
Always return explicitly in catch blocks to satisfy TypeScript's noImplicitReturns:
try {
// ... handler logic
res.json(result);
} catch (error) {
handleError(res, error);
return; // Required even though handleError sends response
}
With conditional response:
} catch (error) {
console.error('Error:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Internal error' });
}
return; // Required for all code paths
}
// apps/api/src/lib/rateLimiter.ts
import { rateLimitAuthEndpoint, rateLimitEndpoint } from '@api/lib/rateLimiter';
// For authenticated endpoints (per-user rate limiting)
router.get(
'/download-data',
rateLimitAuthEndpoint({ windowMs: 24 * 60 * 60 * 1000, max: 3 }),
asyncHandler(AccountController.downloadData)
);
// For public endpoints (IP-based rate limiting)
router.post(
'/create',
rateLimitEndpoint({ windowMs: 10 * 60 * 1000, max: 3 }),
asyncHandler(AccountController.create)
);
// Use asyncHandler to catch errors automatically
import { asyncHandler } from '../lib/asyncHandler';
router.get(
'/:id',
asyncHandler(async (req, res) => {
// Errors here are caught and passed to error middleware
const result = await riskyOperation();
res.json(result);
})
);
See apps/api/src/lib/startup/validation.ts for environment variable validation patterns.
Env file alignment: All .env files (including infra/config/local/*.env) must match the organization, section comments, and variable order of their authoritative .env.example; only values may differ.
Lighthouse alignment: When validation changes here, update
tools/web-perf/lighthouse/.env.api.example and .env.api so the Lighthouse runner
stays in sync with API startup validation.
apps/api/
├── src/
│ ├── controllers/ # Request handlers
│ ├── routes/ # Route definitions
│ ├── middleware/ # Express middleware
│ ├── lib/ # Utilities and helpers
│ │ ├── startup/ # App initialization
│ │ └── rateLimiter.ts
│ └── index.ts # Entry point
├── package.json
└── tsconfig.json
documentation
Per-job env validation and config patterns for the workers app. Use when adding or changing worker commands, touching workers startup validation, or documenting worker env vars.
development
Common patterns and examples for the podverse-web Next.js application
tools
Ensures client-side time displays use formatDateTimeAbbrev for localized, readable timestamps. Use when rendering dates/times in the UI or when the user mentions time formatting or local timezone display.
testing
--- name: podverse-testing-policy description: Skip test implementation unless the user explicitly asks. Use when a plan or task includes adding unit tests, a test phase, or "Phase 3: Tests". version: 1.0.0 --- # Testing policy — skip tests for now ## When to use - When a plan or task includes adding unit tests, a test phase, or "Phase 3: Tests". - When deciding whether to implement tests for a feature. ## Rules 1. **We are not concerning ourselves with tests at this time.** Skip test imp