Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,71 @@ pnpm format:check # Check formatting
- All API routes should have JSDoc comments
- Run `pnpm lint` before committing

## Input Validation

All API endpoints should use a **validate function** for input parsing. Use Zod for schema validation.

### Pattern

Create a `validate<EndpointName>Body.ts` or `validate<EndpointName>Query.ts` file:

```typescript
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { z } from "zod";

// Define the schema
export const createExampleBodySchema = z.object({
name: z.string({ message: "name is required" }).min(1, "name cannot be empty"),
id: z.string().uuid("id must be a valid UUID").optional(),
});

// Export the inferred type
export type CreateExampleBody = z.infer<typeof createExampleBodySchema>;

/**
* Validates request body for POST /api/example.
*
* @param body - The request body
* @returns A NextResponse with an error if validation fails, or the validated body if validation passes.
*/
export function validateCreateExampleBody(body: unknown): NextResponse | CreateExampleBody {
const result = createExampleBodySchema.safeParse(body);

if (!result.success) {
const firstError = result.error.issues[0];
return NextResponse.json(
{
status: "error",
missing_fields: firstError.path,
error: firstError.message,
},
{
status: 400,
headers: getCorsHeaders(),
},
);
}

return result.data;
}
```

### Usage in Handler

```typescript
const validated = validateCreateExampleBody(body);
if (validated instanceof NextResponse) {
return validated;
}
// validated is now typed as CreateExampleBody
```

### Naming Convention

- `validate<Name>Body.ts` - For POST/PUT request bodies
- `validate<Name>Query.ts` - For GET query parameters

## Constants (`lib/const.ts`)

All shared constants live in `lib/const.ts`:
Expand Down
35 changes: 35 additions & 0 deletions app/api/chats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createChatHandler } from "@/lib/chats/createChatHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* POST /api/chats
*
* Create a new chat room.
*
* Authentication: x-api-key header required.
* The account ID is inferred from the API key.
*
* Optional body parameters:
* - artistId: UUID of the artist account the chat is associated with
* - chatId: UUID for the new chat (auto-generated if not provided)
*
* @param request - The request object
* @returns A NextResponse with the created chat or an error
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return createChatHandler(request);
}
68 changes: 68 additions & 0 deletions lib/chats/createChatHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId";
import { insertRoom } from "@/lib/supabase/rooms/insertRoom";
import { generateUUID } from "@/lib/uuid/generateUUID";
import { validateCreateChatBody } from "@/lib/chats/validateCreateChatBody";
import { safeParseJson } from "@/lib/networking/safeParseJson";

/**
* Handler for creating a new chat room.
*
* Requires authentication via x-api-key header.
* The account ID is inferred from the API key.
*
* @param request - The NextRequest object
* @returns A NextResponse with the created chat or an error
*/
export async function createChatHandler(request: NextRequest): Promise<NextResponse> {
try {
const accountIdOrError = await getApiKeyAccountId(request);
if (accountIdOrError instanceof NextResponse) {
return accountIdOrError;
}

const accountId = accountIdOrError;

const body = await safeParseJson(request);

const validated = validateCreateChatBody(body);
if (validated instanceof NextResponse) {
return validated;
}

const { artistId, chatId } = validated;

const roomId = chatId || generateUUID();

const chat = await insertRoom({
id: roomId,
account_id: accountId,
artist_id: artistId || null,
topic: null,
});

return NextResponse.json(
{
status: "success",
chat,
},
{
status: 200,
headers: getCorsHeaders(),
},
);
} catch (error) {
console.error("[ERROR] createChatHandler:", error);
return NextResponse.json(
{
status: "error",
message: "Failed to create chat",
},
{
status: 500,
headers: getCorsHeaders(),
},
);
}
}
37 changes: 37 additions & 0 deletions lib/chats/validateCreateChatBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { z } from "zod";

export const createChatBodySchema = z.object({
artistId: z.string().uuid("artistId must be a valid UUID").optional(),
chatId: z.string().uuid("chatId must be a valid UUID").optional(),
});

export type CreateChatBody = z.infer<typeof createChatBodySchema>;

/**
* Validates request body for POST /api/chats.
*
* @param body - The request body
* @returns A NextResponse with an error if validation fails, or the validated body if validation passes.
*/
export function validateCreateChatBody(body: unknown): NextResponse | CreateChatBody {
const result = createChatBodySchema.safeParse(body);

if (!result.success) {
const firstError = result.error.issues[0];
return NextResponse.json(
{
status: "error",
missing_fields: firstError.path,
error: firstError.message,
},
{
status: 400,
headers: getCorsHeaders(),
},
);
}

return result.data;
}
16 changes: 16 additions & 0 deletions lib/networking/safeParseJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextRequest } from "next/server";

/**
* Safely parses JSON from a request body.
* Returns an empty object if the body is empty or invalid JSON.
*
* @param request - The NextRequest object
* @returns The parsed JSON body or an empty object
*/
export async function safeParseJson(request: NextRequest): Promise<unknown> {
try {
return await request.json();
} catch {
return {};
}
}