feat: add POST /api/routes-b/contacts endpoint#456
feat: add POST /api/routes-b/contacts endpoint#456tranhoangtu-it wants to merge 1 commit intodavedumto:mainfrom
Conversation
Create a new contact for the authenticated user with validation: - name: required, max 100 chars - email: required, valid format - company/notes: optional - Duplicate email per user returns 409 Conflict - Unauthenticated requests return 401 Contact model already exists in schema — no migration needed. Closes davedumto#454
|
@tranhoangtu-it is attempting to deploy a commit to the david's projects Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
Adds a new API endpoint under the routes-b API surface to create Contact records for the authenticated user, using Prisma and the existing Privy token auth helper.
Changes:
- Introduces
POST /api/routes-b/contactsroute handler with basic input validation (name,email) - Implements per-user duplicate email detection and returns
409 Conflict - Creates the contact record and returns a
201with a selected response payload
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export async function POST(request: NextRequest) { | ||
| const authToken = request.headers.get('authorization')?.replace('Bearer ', '') |
There was a problem hiding this comment.
PR/issue description mentions adding a POST handler alongside an existing GET contacts handler in this file, but this route currently only exports POST. If GET is expected for /api/routes-b/contacts, it should be included here; otherwise the PR description/issue scope should be updated to match what’s actually being shipped.
| if ( | ||
| typeof email !== 'string' || | ||
| email.trim() === '' || | ||
| !EMAIL_REGEX.test(email.trim()) | ||
| ) { | ||
| return NextResponse.json( | ||
| { error: 'A valid email address is required.' }, | ||
| { status: 400 }, | ||
| ) | ||
| } | ||
|
|
||
| // Check duplicate email for this user | ||
| const existing = await prisma.contact.findUnique({ | ||
| where: { userId_email: { userId: user.id, email: email.trim() } }, | ||
| }) |
There was a problem hiding this comment.
Email is only trimmed, not normalized (e.g., lowercased). This allows duplicates that differ only by case ("Alice@acme.com" vs "alice@acme.com") to bypass the duplicate check/unique constraint expectations and also diverges from other endpoints that persist emails lowercased (e.g., routes-d contacts/invoices). Consider normalizing to email.trim().toLowerCase() for validation, the duplicate lookup, and the create call.
| // Check duplicate email for this user | ||
| const existing = await prisma.contact.findUnique({ | ||
| where: { userId_email: { userId: user.id, email: email.trim() } }, | ||
| }) | ||
| if (existing) { | ||
| return NextResponse.json( | ||
| { error: 'A contact with this email already exists.' }, | ||
| { status: 409 }, | ||
| ) | ||
| } | ||
|
|
||
| const contact = await prisma.contact.create({ | ||
| data: { | ||
| userId: user.id, | ||
| name: name.trim(), | ||
| email: email.trim(), | ||
| company: typeof company === 'string' ? company.trim() || null : null, | ||
| notes: typeof notes === 'string' ? notes.trim() || null : null, | ||
| }, | ||
| select: { | ||
| id: true, | ||
| name: true, | ||
| email: true, | ||
| company: true, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
The duplicate check + create is subject to a race condition: two concurrent requests can both pass findUnique and then one will fail on the DB unique constraint during create, likely returning a 500 instead of the required 409. Wrap the prisma.contact.create in a try/catch and map Prisma unique constraint errors (P2002) to a 409 response (or rely solely on catching P2002 without the pre-check).
Summary
POST /api/routes-b/contactsendpoint inapp/api/routes-b/contacts/route.tsbank-accounts/route.tsValidation
name: required, max 100 charsemail: required, valid format (regex)company,notes: optional strings[userId, email]→409 Conflict401Test plan
POSTwith valid body →201with created contact409400400401Closes #454