| name |
description |
| add-api-route |
Add a new authenticated API route under pages/api/. Use when you need to expose a new server endpoint to the client, scaffold an admin-only route, or add a CRUD method to an existing resource. Walks through file placement, auth, validation, DB access, and error handling for the tcg-vault stack. |
Add an API route
pages/api/<path>.js becomes /api/<path>. Dynamic segments use [name] folder/file naming.
Step 1: Decide the path
| Pattern |
Example |
Notes |
| Resource collection |
pages/api/decks.js → /api/decks |
GET list, POST create |
| Single resource |
pages/api/decks/[id].js → /api/decks/:id |
GET, PUT, DELETE |
| Sub-resource |
pages/api/decks/[id]/cards.js |
GET, POST |
| Action |
pages/api/cards/find-or-create.js |
POST, RPC-style |
Step 2: Pick the auth pattern
| Use case |
Wrapper |
| Generic logged-in user |
getUserFromRequest(req) inline |
| Collection-scoped op |
withCollectionPermission('viewer'|'editor'|'owner') |
| Admin-only |
inline if (user.role !== 'admin') return res.status(403) |
Step 3: Skeleton
import { sql } from '@vercel/postgres';
import { getUserFromRequest } from '../../lib/permission-middleware';
export default async function handler(req, res) {
// 1. Method gate
if (!['GET', 'POST'].includes(req.method)) {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// 2. Auth
const user = await getUserFromRequest(req);
if (!user || !req.headers.authorization) {
// Guard against the known dev-fallback bug; require real Bearer token.
return res.status(401).json({ error: 'Authentication required' });
}
if (req.method === 'GET') {
const { rows } = await sql`
SELECT id, name FROM example_table WHERE user_id = ${user.userId}
`;
return res.status(200).json({ items: rows });
}
// 3. Validate body
const { name } = req.body || {};
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({ error: 'name is required' });
}
// 4. Mutation
const { rows } = await sql`
INSERT INTO example_table (user_id, name)
VALUES (${user.userId}, ${name.trim()})
RETURNING id, name
`;
return res.status(201).json({ item: rows[0] });
} catch (err) {
console.error('[example handler]', err);
return res.status(500).json({ error: 'Internal server error' });
}
}
Step 4: Wire the client
Use fetch with the auth header pattern from existing pages:
const token = localStorage.getItem('auth_token');
const res = await fetch('/api/example', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ name }),
});
Step 5: Check
Anti-patterns
| Don't |
Do |
db.query(\SELECT … ${userInput}`)` |
await sql\SELECT … ${userInput}`` |
| Forget the method gate |
Always declare which methods are allowed |
Return res.json(err.message) in catch |
Generic message; log details server-side |
SELECT * from cards |
Project narrow columns |
Add another /api/test-* endpoint |
Use a real test runner once one's adopted |