7.8 KiB
7.8 KiB
Frontend API Guide (HotTRDeals)
This file is a frontend-focused summary of current backend routes, auth, and response models.
Server entry: server.js.
Base URL and CORS
- Local API base:
http://localhost:3000 - All routes below are relative to
/api. - CORS in dev allows
http://localhost:5173andcredentials: true.
Auth summary
- Access token is returned in response body:
{ token, user }. - Send access token via header:
Authorization: Bearer <token>. - Refresh token is stored in httpOnly cookie named
rt. - For
/auth/refreshand/auth/logout, the frontend must send cookies (withCredentials: true). - Cookie options:
- Dev:
secure=false,sameSite=lax - Prod:
secure=true,sameSite=none
- Dev:
Roles
- Roles:
USER,MOD,ADMIN. requireRole("MOD")means only MOD and ADMIN can access.
Common response shapes
- Errors are inconsistent:
- Some endpoints return
{ error: "..." } - Some endpoints return
{ message: "..." }
- Some endpoints return
- Expect both in frontend error handling.
Common models (from adapters/contracts)
PublicUserSummary
{
id: number,
username: string,
avatarUrl: string | null
}
PublicUserDetails
{
id: number,
username: string,
avatarUrl: string | null,
email: string,
createdAt: string (ISO)
}
AuthUser
{
id: number,
username: string,
email: string,
role: "USER" | "MOD" | "ADMIN",
avatarUrl: string | null
}
DealCard
{
id: number,
title: string,
description: string,
price: number | null,
originalPrice?: number | null,
shippingPrice?: number | null,
score: number,
commentsCount: number,
url: string | null,
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED",
saleType: "ONLINE" | "OFFLINE" | "CODE",
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE",
myVote: -1 | 0 | 1,
createdAt: string (ISO),
updatedAt: string (ISO),
user: PublicUserSummary,
seller: { name: string, url: string | null },
imageUrl: string
}
DealDetail
{
id: number,
title: string,
description: string,
url: string | null,
price: number | null,
originalPrice?: number | null,
shippingPrice?: number | null,
score: number,
commentsCount: number,
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED",
saleType: "ONLINE" | "OFFLINE" | "CODE",
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE",
createdAt: string (ISO),
updatedAt: string (ISO),
user: PublicUserSummary,
seller: { id: number, name: string, url: string | null },
images: [{ id: number, imageUrl: string, order: number }],
comments: [{ id: number, text: string, createdAt: string, parentId?: number | null, user: PublicUserSummary }],
breadcrumb: [{ id: number, name: string, slug: string }],
notice: {
id: number,
dealId: number,
title: string,
body: string | null,
severity: "INFO" | "WARNING" | "DANGER" | "SUCCESS",
isActive: boolean,
createdBy: number,
createdAt: string,
updatedAt: string
} | null,
similarDeals: [{ id: number, title: string, price: number | null, score: number, imageUrl: string, sellerName: string, createdAt: string | null }]
}
DealListResponse
{
page: number,
total: number,
totalPages: number,
results: DealCard[]
}
Comment (DealComment)
{
id: number,
text: string,
createdAt: string,
parentId?: number | null,
user: PublicUserSummary
}
UserProfile
{
user: PublicUserDetails,
stats: { totalLikes: number, totalShares: number, totalComments: number, totalDeals: number },
deals: DealCard[],
comments: [{ ...Comment, deal: { id: number, title: string } }]
}
VoteResponse
{
dealId: number,
voteType: -1 | 0 | 1,
delta: number,
score: number | null
}
VoteListResponse
{
votes: [{ id, dealId, userId, voteType, createdAt, lastVotedAt }]
}
Endpoints
Auth (/api/auth)
-
POST /register- Body:
{ username, email, password } - Response:
{ token, user: AuthUser }and setsrtcookie if refresh token exists.
- Body:
-
POST /login- Body:
{ email, password } - Response:
{ token, user: AuthUser }and setsrtcookie.
- Body:
-
POST /refresh- Cookie required:
rt - Response:
{ token, user: AuthUser }and rotatesrtcookie.
- Cookie required:
-
POST /logout- Cookie optional:
rt - Response:
204 No Content, clearsrtcookie.
- Cookie optional:
-
GET /me- Auth: required
- Response:
AuthUser - Note: current implementation reads
req.user.userIdin adapter, while auth middleware setsreq.auth. Ifreq.useris undefined, this endpoint can fail.
Account (/api/account)
-
POST /avatar- Auth: required
- Multipart:
file(single) - Validation: JPEG only, max 2MB
- Response:
{ message, user: PublicUserSummary }
-
GET /me- Auth: required
- Response:
PublicUserDetails
Deals (/api/deals)
List query params (used in list endpoints):
q(string, default "")page(int, default 1)limit(int, default 10, max 100)
Endpoints:
-
GET /users/:userName/deals- Auth: optional
- Response:
DealListResponse
-
GET /me/deals- Auth: required
- Response:
DealListResponse
-
GET /new -
GET /hot -
GET /trending -
GET /(same as/new)- Auth: optional
- Response:
DealListResponse
-
GET /search- Auth: optional
- If
qis empty:{ results: [], total: 0, totalPages: 0, page } - Else:
DealListResponse
-
GET /top- Auth: optional
- Query:
range=day|week|month(defaultday),limit(default 6, max 20) - Response:
DealCard[](array only)
-
GET /:id- Response:
DealDetail
- Response:
-
POST /- Auth: required
- Multipart:
images(array, max 5) - Limits: 10MB per file (upload middleware)
- Body fields:
{ title, description?, url?, price?, sellerName? } - Response:
DealDetail
Category (/api/category)
-
GET /:slug- Response:
{ id, name, slug, description, breadcrumb }
- Response:
-
GET /:slug/deals- Auth: optional
- Query:
page,limit, plus filters from query - Response:
DealCard[](array only)
Seller (/api/seller)
-
POST /from-link- Auth: required
- Body:
{ url } - Response:
{ found: boolean, seller: { id, name, url } | null }
-
GET /:sellerName- Response:
{ id, name, url, logoUrl }
- Response:
-
GET /:sellerName/deals- Auth: optional
- Query:
page,limit,q - Response:
DealCard[](array only)
Comments (/api/comments)
-
GET /:dealId- Response:
Comment[]
- Response:
-
POST /- Auth: required
- Body:
{ dealId, text, parentId? } - Response:
Comment
-
DELETE /:id- Auth: required
- Response:
{ message: "Yorum silindi." }
Votes (/api/vote and /api/deal-votes)
-
POST /- Auth: required
- Body:
{ dealId, voteType }where voteType is -1, 0, or 1 - Response:
VoteResponse
-
GET /:dealId- Response:
VoteListResponse
- Response:
Users (/api/user and /api/users)
GET /:userName- Response:
UserProfile
- Response:
Mod (/api/mod) (MOD or ADMIN)
-
GET /deals/pending- Auth + Role: MOD
- Query:
page,limit,qplus filters - Response:
DealCard[](array only)
-
POST /deals/:id/approve -
POST /deals/:id/reject -
POST /deals/:id/expire -
POST /deals/:id/unexpire- Auth + Role: MOD
- Response:
{ id, status }
Notes for frontend
- Some list endpoints return full pagination object, some return only
resultsarray. Treat them separately:- Full:
/api/dealslist endpoints,/api/deals/search,/api/deals/users/:userName/deals,/api/deals/me/deals - Array only:
/api/deals/top,/api/category/:slug/deals,/api/seller/:sellerName/deals,/api/mod/deals/pending
- Full:
- Optional-auth endpoints return
401if a token is provided but invalid. - MyVote is only filled when Authorization header is present.
- There are duplicate route prefixes for users and votes. Frontend can use either, but pick one for consistency.