| name | api-endpoint |
| description | Create new REST API endpoints following project patterns. Use when adding API routes, creating actions/loaders, implementing CRUD operations, or when the user mentions API, endpoint, action, or REST. |
| allowed-tools | ["Read","Write","Edit","Glob","Grep"] |
Creating API Endpoints
Create REST API endpoints following Pixel Studio's established patterns.
File Naming Convention
API routes go in app/routes/ with dot-separated naming:
api.resource.ts # Collection endpoint
api.resource.$id.ts # Single resource
api.resource.action.ts # Specific action
api.resource.$id.action.ts # Action on single resource
Standard Structure
import {
json,
type ActionFunctionArgs,
type LoaderFunctionArgs,
} from "@remix-run/node";
import { z } from "zod";
import { requireUserLogin } from "~/server/auth.server";
import { prisma } from "~/services/prisma.server";
const RequestSchema = z.object({
field: z.string().min(1),
});
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireUserLogin(request);
const data = await prisma.resource.findMany({
where: { userId: user.id },
orderBy: { createdAt: "desc" },
});
return json({ data });
}
export async function action({ request, params }: ActionFunctionArgs) {
const user = await requireUserLogin(request);
if (request.method === "DELETE") {
await prisma.resource.delete({ where: { id: params.id } });
return json({ success: true });
}
const formData = await request.formData();
const data = RequestSchema.parse(Object.fromEntries(formData));
const result = await prisma.resource.create({
data: { ...data, userId: user.id },
});
return json({ success: true, data: result });
}
Authentication Patterns
const user = await requireUserLogin(request);
const auth = await getGoogleSessionAuth(request);
const userId = auth?.user?.id;
await requireAnonymous(request);
Error Handling
try {
const data = RequestSchema.parse(formData);
} catch (error) {
if (error instanceof z.ZodError) {
return json({ error: error.errors[0].message }, { status: 400 });
}
}
const resource = await prisma.resource.findUnique({ where: { id } });
if (!resource) {
return json({ error: "Resource not found" }, { status: 404 });
}
if (resource.userId !== user.id) {
return json({ error: "Not authorized" }, { status: 403 });
}
return json({ error: "Failed to process request" }, { status: 500 });
Cache Integration
import { getCachedDataWithRevalidate, cacheDelete } from "~/utils/redis-cache";
const data = await getCachedDataWithRevalidate(
`resource:${userId}`,
() => prisma.resource.findMany({ where: { userId } }),
60 * 5,
);
await cacheDelete(`resource:${userId}`);
Response Patterns
return json({ success: true });
return json({ data: result });
return json({ collection, message: "Created successfully" });
return json({ error: "Validation failed" }, { status: 400 });
return json({ error: "Not found" }, { status: 404 });
return json({ error: "Not authorized" }, { status: 403 });
Example: Like/Unlike Endpoint
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { requireUserLogin } from "~/server/auth.server";
import { likeImage, unlikeImage } from "~/server/imageLikes";
import { cacheDelete } from "~/utils/redis-cache";
export async function action({ request, params }: ActionFunctionArgs) {
const user = await requireUserLogin(request);
const { imageId } = params;
if (!imageId) {
return json({ error: "Image ID required" }, { status: 400 });
}
if (request.method === "POST") {
await likeImage(imageId, user.id);
} else if (request.method === "DELETE") {
await unlikeImage(imageId, user.id);
}
await cacheDelete(`explore-images`);
return json({ success: true });
}
Checklist