Skip to content

Commit 60ba062

Browse files
committed
feat: add POST /profiles endpoint with validation and duplicate handling
- Add sendError helper for consistent error responses - Add createProfileSchema with Zod validation - Create profile with nested acceptedAssets in single Prisma call - Return 400 for invalid body, 409 for duplicate username, 201 on success
1 parent d905e50 commit 60ba062

2 files changed

Lines changed: 81 additions & 35 deletions

File tree

backend/package-lock.json

Lines changed: 34 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/src/app.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import cors from "cors";
2-
import express from "express";
2+
import express, { Response } from "express";
33
import { z } from "zod";
44
import { prisma } from "./db.js";
55

6+
function sendError(res: Response, status: number, message: string, code?: string) {
7+
return res.status(status).json({ error: message, ...(code ? { code } : {}) });
8+
}
9+
610
export function createApp() {
711
const app = express();
812

@@ -33,6 +37,48 @@ export function createApp() {
3337
res.json(profile);
3438
});
3539

40+
const createProfileSchema = z.object({
41+
username: z.string().min(3).max(32).regex(/^[a-z0-9-]+$/),
42+
displayName: z.string().min(1).max(64),
43+
bio: z.string().max(280).optional().default(""),
44+
walletAddress: z.string().startsWith("G").length(56),
45+
ownerId: z.string().min(1),
46+
acceptedAssets: z.array(z.object({
47+
code: z.string().min(1).max(12),
48+
issuer: z.string().optional(),
49+
})).min(1),
50+
});
51+
52+
app.post("/profiles", async (req, res) => {
53+
const parsed = createProfileSchema.safeParse(req.body);
54+
55+
if (!parsed.success) {
56+
return sendError(res, 400, "Invalid request body");
57+
}
58+
59+
const { username, displayName, bio, walletAddress, ownerId, acceptedAssets } = parsed.data;
60+
61+
try {
62+
const profile = await prisma.profile.create({
63+
data: {
64+
username,
65+
displayName,
66+
bio,
67+
walletAddress,
68+
ownerId,
69+
acceptedAssets: { create: acceptedAssets },
70+
},
71+
include: { acceptedAssets: true },
72+
});
73+
return res.status(201).json(profile);
74+
} catch (e: unknown) {
75+
if (e && typeof e === "object" && "code" in e && e.code === "P2002") {
76+
return sendError(res, 409, "Username already taken", "USERNAME_TAKEN");
77+
}
78+
return sendError(res, 500, "Internal server error");
79+
}
80+
});
81+
3682
const supportPayloadSchema = z.object({
3783
txHash: z.string().min(3),
3884
amount: z.string().min(1),

0 commit comments

Comments
 (0)