Skip to content

How to Add a New API Route

Use this guide when adding any new backend endpoint in show.fm.

Non-Negotiable Rules

  • All backend APIs must live under src/api/routes/ and be mounted in src/api/index.ts.
  • Do not create SvelteKit +server.ts files for API endpoints.
  • Use shared auth/permission middleware from src/api/middleware/.
  • Do not add SUPABASE_SERVICE_ROLE_KEY fallback logic in new route code.

Required Middleware Chain

Use the smallest chain that satisfies the endpoint's security needs.

  1. requireAuth() for authenticated endpoints
  2. zValidator(...) for body/query/param validation
  3. requirePodcastRole(...) or resolver-based variant for podcast-scoped authorization
  4. Handler function

Example ordering:

ts
.post(
  '/',
  requireAuth(),
  zValidator('json', createSchema),
  requirePodcastRole('admin'),
  async (c) => {
    // handler logic
  }
)

Permission Patterns

  • requireAuth()
    Use when endpoint needs a logged-in user but no podcast/episode scope.

  • requirePodcastRole('member' | 'admin' | 'owner')
    Use when podcastId (or podcast_id) is available in params/query/body.

  • requirePodcastRoleByResolver(minRole, resolver)
    Use when podcast scope must be looked up from another resource ID first.

  • requirePodcastRoleOrEpisodeRoster(episodeResolver) (#293)
    The roster-aware guard for episode-scoped reads that Co-hosts reach: passes admin+, or a member holding an ACTIVE episode_people roster row on the resolved episode. A missing episode 404s; sets podcastRole, podcastId, and episodeId on context and preserves the lifecycle block.

  • requireEpisodeAccess() / requireEpisodePermission(...)
    Use for episode-scoped authorization.

Error Response Format

Keep errors consistent and machine-readable:

  • Prefer throwing HTTPException for auth/permission/not-found checks in middleware/resolvers.
  • Return explicit handler errors as c.json({ error: 'message' }, statusCode).
  • Use consistent status codes:
    • 400 invalid input
    • 401 unauthenticated
    • 403 unauthorized
    • 404 not found
    • 409 conflict
    • 500 server error

Correct Route Example

ts
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
import { requireAuth, requirePodcastRoleByResolver, type PermissionEnv } from '$api/middleware';

type ExampleEnv = {
	Bindings: PermissionEnv['Bindings'];
	Variables: PermissionEnv['Variables'];
};

const updateSchema = z.object({
	name: z.string().min(1).max(100)
});

const resolvePodcastId = async ({
	c,
	supabase
}: {
	c: { req: { param: (key: string) => string } };
	supabase: PermissionEnv['Variables']['supabase'];
}) => {
	const resourceId = c.req.param('id');
	const { data, error } = await supabase
		.from('some_resources')
		.select('podcast_id')
		.eq('id', resourceId)
		.single();

	if (error || !data) {
		throw new HTTPException(404, { message: 'Resource not found' });
	}

	return data.podcast_id;
};

export const exampleRoutes = new Hono<ExampleEnv>().put(
	'/:id',
	requireAuth(),
	zValidator('json', updateSchema),
	requirePodcastRoleByResolver('admin', resolvePodcastId),
	async (c) => {
		const { name } = c.req.valid('json');
		const supabase = c.get('supabase');
		const id = c.req.param('id');

		const { error } = await supabase.from('some_resources').update({ name }).eq('id', id);
		if (error) {
			return c.json({ error: 'Failed to update resource' }, 500);
		}

		return c.json({ success: true });
	}
);

Mounting and Type Safety

  1. Export route group from src/api/routes/<route>.ts.
  2. Mount it in src/api/index.ts with .route('/<route>', <routeGroup>).
  3. Keep API client usage on the frontend as client.api... (not client...).

Test Checklist

Add tests adjacent to the route code (for example, src/api/routes/<group>/__tests__/index.test.ts):

  • Missing token returns 401
  • Insufficient role returns 403
  • Cross-tenant access is denied
  • Validation failures return 400
  • Happy path succeeds

Documentation Checklist

When adding a route:

  1. Add/Update route reference in docs-internal/src/api/
  2. Update this guide if a new middleware pattern is introduced
  3. Keep examples aligned with current middleware exports in src/api/middleware/index.ts

Internal documentation - Not for public distribution