- name
- elysia-api-routes
- description
- Create and update QuickStack Elysia REST API routes using the project's established /api/v1 route conventions. Use when adding or editing files under src/server/api/v1, defining Elysia query/params/body/response schemas, or handling REST API authorization and errors.
# Elysia API Routes
## Quick Start
For QuickStack REST routes under `src/server/api/v1`, follow the current examples in `app/route.ts` and `project/route.ts`:
```ts
export const resourceRoutes = new Elysia()
.derive(ApiUtils.deriveFunc)
.get('/resources/:id', async ({ params, identity }) => {
if (!identity) throw new ApiUnauthorizedException()
const resource = await resourceService.getByIdOrUndefined(params.id);
if (!resource) throw new ApiNotFoundException();
ensureReadResource(identity, resource.id);
return resource;
}, {
params: z.object({
id: z.string(),
}),
response: ApiUtils.mapResponseModel(ResourceModel),
detail: { summary: 'Get resource by id', security: [{ bearerAuth: [] }] }
});
```
## Required Route Shape
- Start each route module with `new Elysia().derive(ApiUtils.deriveFunc)` so handlers receive `identity`.
- Import `ApiUtils` from `src/server/utils/api-response.utils`.
- Import `ApiUnauthorizedException`, `ApiNotFoundException`, and `ServiceException` from `src/shared/model/service.exception.model` as needed.
- Declare `query`, `params`, and `body` directly in route options with Zod schemas.
- Declare `response` with `ApiUtils.mapResponseModel(successSchema)`.
- Keep OpenAPI metadata in `detail`, with a short `summary` and `security: [{ bearerAuth: [] }]` for protected routes.
## Handler Rules
- If `identity` is missing, throw `new ApiUnauthorizedException()`.
- If a requested resource does not exist, throw `new ApiNotFoundException()`.
- Use shared authorization helpers such as `ensureReadApp`, `ensureWriteApp`, `ensureCreateAppInProject`, `ensureDeleteAppInProject`, `ensureReadProject`, and `ensureAdmin`.
- Let shared authorization helpers throw; do not duplicate permission checks inline except for simple admin/read filtering already established in list routes.
- Throw `ServiceException` for expected domain validation errors, such as immutable `projectId` violations.
- Return success payloads directly; do not wrap them in `{ data }`, `{ status }`, or error envelopes.
- Do not return `ApiUtils.problem(...)`, raw `Response`, or Elysia `status(...)` for expected route errors.
## Schema Rules
- Use inline Zod objects for simple route params and query inputs.
- Use existing write schemas, such as `AppExtendedWriteZodModel` or a local `projectWriteSchema`, for bodies.
- Do not parse `query`, `params`, or `body` inside the handler if the route option already declares the schema.
- Do not use nested `schema: { query, params, body }` in these route modules.
- For delete routes, return `undefined` and declare `response: ApiUtils.mapResponseModel(z.undefined())`.
- For deployment request routes, return `{ deploymentId }` and declare `response: ApiUtils.mapResponseModel(z.object({ deploymentId: z.string() }))`.
## Write Route Pattern
Use POST upsert semantics:
```ts
.post('/projects', async ({ body, identity }) => {
if (!identity) throw new ApiUnauthorizedException()
ensureAdmin(identity);
let existing: Project | null = null;
if (body.id) {
existing = await projectService.getByIdOrUndefined(body.id);
if (!existing) throw new ApiNotFoundException();
}
return projectService.save({ id: existing?.id, name: body.name });
}, {
body: projectWriteSchema,
response: ApiUtils.mapResponseModel(ProjectModel),
detail: { summary: 'Create or update project', security: [{ bearerAuth: [] }] }
})
```
## Validation Checklist
- Run `yarn tsc --noEmit` after route changes.
- Check that every accepted input has a route-level Zod schema.
- Check that every route has `response: ApiUtils.mapResponseModel(...)`.
- Check that expected failures are thrown as exceptions; route mounting maps them centrally with `ApiUtils.mapError(...)`.
- Check `CONTEXT.md` for REST API domain terms and write semantics before changing behavior.
GitHub에서 보기