.agents/skills/api-route/SKILL.md
Add a new Hono API route or server-side utility. Use when adding endpoints, handlers, reusable server logic, validation helpers, or data transformation functions.
npx skillsauth add vigneshksaithal/urjo add-api-routeInstall 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.
All code must follow the Coding Principles in AGENTS.md (functional, minimal, readable, modular).
src/server/index.tssrc/server/routes/{feature-name}.ts and import into src/server/index.tssrc/server/lib/{feature-name}.ts/api/kebab-case/internal/menu/action-name/internal/on-event-nameimport { Hono } from 'hono'
import type { Context } from 'hono'
import { context, redis, reddit } from '@devvit/web/server'
const HTTP_STATUS_BAD_REQUEST = 400
const HTTP_STATUS_FORBIDDEN = 403
const HTTP_STATUS_INTERNAL_ERROR = 500
const myHandler = async (c: Context): Promise<Response> => {
try {
const userId = requireUserId()
const result = await doThing()
return c.json({ status: 'success', data: result })
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return c.json({ status: 'error', message }, HTTP_STATUS_INTERNAL_ERROR)
}
}
app.get('/api/my-route', myHandler)
// Success: { status: 'success', data: { ... } }
// Error: { status: 'error', message: 'Human-readable string' }
// Menu navigation: { navigateTo: 'https://reddit.com/...' }
type SuccessResponse<T> = { status: 'success'; data: T }
type ErrorResponse = { status: 'error'; message: string }
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse
Extract these into src/server/lib/context-guards.ts when used across multiple routes:
import { context } from '@devvit/web/server'
export const requireUserId = (): string => {
const { userId } = context
if (!userId) throw new Error('User must be logged in')
return userId
}
export const requirePostId = (): string => {
const { postId } = context
if (!postId) throw new Error('Must be in a post context')
return postId
}
For reusable server logic in src/server/lib/:
export const parseRedisNumber = (value: string | undefined, fallback: number): number => {
if (value === undefined) return fallback
const parsed = parseInt(value, 10)
return Number.isNaN(parsed) ? fallback : parsed
}
export const getErrorMessage = (error: unknown): string => {
if (error instanceof Error) return error.message
return 'Unknown error'
}
Every /api/* endpoint is callable by any Reddit user who loads the post. Treat all client input as untrusted.
const handler = async (c: Context): Promise<Response> => {
const body = await c.req.json().catch(() => null)
if (!body || typeof body !== 'object') {
return c.json({ status: 'error', message: 'Invalid request body' }, HTTP_STATUS_BAD_REQUEST)
}
// Validate each field — never spread or destructure blindly
const { guess } = body as Record<string, unknown>
if (typeof guess !== 'string' || guess.length === 0 || guess.length > 200) {
return c.json({ status: 'error', message: 'Invalid guess' }, HTTP_STATUS_BAD_REQUEST)
}
}
| ❌ Never | ✅ Instead |
|----------|-----------|
| Use await c.req.json() then destructure directly | Parse with .catch(() => null), check shape |
| Trust typeof x === 'string' alone | Also check length bounds |
| Accept numeric input as-is | parseInt + Number.isNaN + range check |
| Pass raw user input into Redis keys | Build keys from validated/server-sourced values only |
type SubmitGuessInput = { guess: string; difficulty: 'easy' | 'medium' | 'hard' }
const VALID_DIFFICULTIES = ['easy', 'medium', 'hard'] as const
const parseSubmitGuess = (raw: unknown): SubmitGuessInput | null => {
if (!raw || typeof raw !== 'object') return null
const obj = raw as Record<string, unknown>
const { guess, difficulty } = obj
if (typeof guess !== 'string' || guess.length === 0 || guess.length > 500) return null
if (typeof difficulty !== 'string') return null
if (!VALID_DIFFICULTIES.includes(difficulty as typeof VALID_DIFFICULTIES[number])) return null
return { guess, difficulty: difficulty as SubmitGuessInput['difficulty'] }
}
// Usage:
const input = parseSubmitGuess(await c.req.json().catch(() => null))
if (!input) {
return c.json({ status: 'error', message: 'Invalid input' }, HTTP_STATUS_BAD_REQUEST)
}
context.userId// ❌ WRONG: client tells us who they are
const { userId } = await c.req.json()
// ✅ RIGHT: server knows who they are
const { userId } = context
const owner = await redis.hGet(`game:${postId}:meta`, 'creatorId')
if (owner !== userId) {
return c.json({ status: 'error', message: 'Not authorized' }, HTTP_STATUS_FORBIDDEN)
}
context.* values or validated/bounded strings in keys: — it's your key delimiter/^[a-zA-Z0-9]{4,8}$/setInterval, no long-running processes (30s request timeout)media.upload()src/server/__tests__/ using bun:test and devvit-mocksinstanceof Error narrowingcontext.userId / context.postId guarded before use.catch(() => null) and shape-validatedcontext.userId, never from request bodybun run test passes with zero failurestools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
A CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.