The agreement between the front end and the back end. The back end implements these endpoints exactly. The front end can build against them before they exist by using fake data in the same shape.
Field rules and value lists are in data-model.md. Behavior rules marked D-xx are in decisions.md.
Base URL: /api (the Vite dev server proxies it to port 5000). Request and response bodies are JSON, except photo uploads.
Auth: Send the token as Authorization: Bearer <token>. client/src/lib/api.js already does this when a token is in localStorage under findorm_token.
Who can call it is listed on every endpoint:
- Public — no token needed.
- Logged in — any active user.
- Seeker / Owner / Admin — only that role. Other roles get
403. - (own) — only the owner of that record. Admins are allowed only where noted.
Errors always have this shape (matches server/middleware/errorHandler.js):
{ "message": "Monthly rent must be at least 1." }Validation errors (400) also list each bad field, so forms can show the message next to the right input:
{
"message": "Please fix the highlighted fields.",
"errors": {
"monthlyRent": "Monthly rent must be at least 1.",
"city": "Choose a city in Metro Manila."
}
}| Status | Meaning |
|---|---|
400 |
Invalid input (NFR-05) |
401 |
No token, bad or expired token, or account deactivated. The front end should clear the token and go to login |
403 |
Logged in, but this role or user isn't allowed (NFR-02) |
404 |
Not found — also used when the record exists but belongs to someone else and the caller shouldn't know it exists |
409 |
Conflicts with current state: email taken, listing full, duplicate request, request no longer Pending |
Paged lists return:
{ "items": [ ... ], "page": 1, "limit": 12, "total": 40, "totalPages": 4 }Query page (default 1) and limit (default 12, max 50).
Objects returned — the same shapes are reused below:
- User —
{ _id, firstName, lastName, email, phone, role, isActive, createdAt }. Never includespassword. - PublicUser —
{ _id, firstName, lastName }. Used when showing another person (e.g. listing owner, message sender). - Listing — every listing field from the data model, plus
isFull, withowneras a PublicUser. - ListingSummary —
{ _id, name, propertyType, city, monthlyRent, genderCategory, availableSlots, capacity, isFull, photo }wherephotois the first photo'surlornull. Used in search results and lists.
Create a seeker or owner account (FR-01). Logs the user in straight away.
{
"firstName": "Juan",
"lastName": "Dela Cruz",
"email": "juan@example.com",
"password": "at-least-8-chars",
"role": "seeker",
"phone": "09171234567"
}role must be seeker or owner (D-09). phone is optional.
201 → { "token": "...", "user": User }
400 invalid fields or role: "admin" · 409 email already registered
{ "email": "juan@example.com", "password": "..." }200 → { "token": "...", "user": User }
401 wrong email or password — use the same message for both so attackers can't find which emails exist · 401 account deactivated — message says so
Logout has no endpoint. The front end deletes findorm_token (D-13).
Current user. The front end calls this on page load to check a saved token is still valid.
200 → User
Update own details (FR-03). Send only the fields to change: firstName, lastName, email, phone. Any other field (like role or isActive) is ignored.
200 → User · 400 · 409 email taken
{ "currentPassword": "...", "newPassword": "..." }204 no body · 400 new password too short · 401 current password wrong
Search and filter (FR-07, FR-08, FR-09). All filters are optional and combine with AND. With no filters, returns every listing, full ones included.
| Query | Example | Effect |
|---|---|---|
city |
Manila |
Exact city (D-01) |
q |
sampaloc |
Keyword, case-insensitive, matches name or address |
minPrice |
3000 |
monthlyRent ≥ this |
maxPrice |
6000 |
monthlyRent ≤ this |
propertyType |
Dormitory |
Exact type |
gender |
Female |
See D-12 — Female returns Female and Any |
available |
true |
Only listings with availableSlots > 0 |
sort |
price_asc |
newest (default), price_asc, price_desc |
page, limit |
Paging |
Unknown values (e.g. city=Cebu) return 400, not an empty list, so typos are caught early.
200 → paged list of ListingSummary
The logged-in owner's own listings, newest first. Includes full listings.
200 → paged list of ListingSummary
Full details (FR-10).
200 → Listing · 404
Create a listing (FR-04, FR-05). Photos are added afterwards with the photo endpoint.
{
"name": "Casa Verde Dormitory",
"propertyType": "Dormitory",
"city": "Manila",
"address": "123 P. Noval St., Sampaloc",
"description": "5 minutes walk to UST.",
"monthlyRent": 4500,
"genderCategory": "Female",
"amenities": ["WiFi", "Study Area", "CCTV"],
"houseRules": "Curfew 10 PM. No visitors in rooms.",
"capacity": 20,
"availableSlots": 6
}owner comes from the token, never the body.
201 → Listing · 400
Edit a listing (FR-04, FR-17). Send only the fields to change — same fields as create. If capacity is lowered below the current availableSlots, return 400.
200 → Listing · 400 · 404
Update available slots (FR-06). A separate endpoint so the owner's dashboard can change it quickly.
{ "availableSlots": 3 }Must be 0 to capacity.
200 → Listing · 400 · 404
Deletes the listing, its reservations, its inquiries, and its photos (D-11).
204 · 404
multipart/form-data with one or more files in the field photos. JPEG, PNG, or WebP, 5 MB each, 10 photos per listing in total.
200 → Listing · 400 wrong type, too big, or over 10 · 404
photoId is the photo's _id inside the listing. Also deletes it from Cloudinary.
200 → Listing · 404
Request one slot (FR-13, D-03).
{ "listingId": "...", "moveInDate": "2026-11-01", "message": "Hi, I'm a 2nd year student at UST." }moveInDate and message are optional.
201 → Reservation (see below)
400 · 404 listing not found · 409 listing is full (D-04) · 409 you already have a Pending or Accepted request for this listing
The seeker's own requests with their status (FR-15), newest first. Optional status filter.
200 → paged list of Reservation
Requests for the owner's listings (FR-14), newest first. Optional filters: status, listingId.
200 → paged list of Reservation
Accept a Pending request (FR-14). Decreases the listing's availableSlots by 1 (FR-16).
200 → Reservation
404 · 409 not Pending anymore · 409 listing is full — no slots left (FR-16, D-04)
Reject a Pending request (FR-14). Works even when the listing is full. Slots are unchanged.
200 → Reservation · 404 · 409 not Pending anymore
Withdraw a Pending request (D-05).
204 · 404 · 409 not Pending anymore
Reservation object:
{
"_id": "...",
"listing": { "_id": "...", "name": "Casa Verde Dormitory", "city": "Manila", "monthlyRent": 4500, "availableSlots": 5, "isFull": false },
"seeker": { "_id": "...", "firstName": "Juan", "lastName": "Dela Cruz" },
"status": "Pending",
"moveInDate": "2026-11-01T00:00:00.000Z",
"message": "Hi, I'm a 2nd year student at UST.",
"respondedAt": null,
"createdAt": "2026-10-02T08:15:00.000Z"
}Ask about a listing (FR-11). If the seeker already has a thread for this listing, the message is added to it instead (D-07).
{ "listingId": "...", "body": "Is water included in the rent?" }201 new thread / 200 added to existing thread → Inquiry · 400 · 404 listing not found
The user's threads, most recent activity first. A seeker sees threads they started; an owner sees threads about their listings (FR-12). Messages are not included — just the latest one as a preview.
200 → paged list of { _id, listing: { _id, name }, seeker: PublicUser, lastMessage: { body, sender, createdAt }, lastMessageAt }
The full thread.
200 → Inquiry · 404 (also when the caller isn't part of the thread — NFR-04)
Reply in a thread (FR-12). Either side can post.
{ "body": "Yes, water is included." }201 → Inquiry · 400 · 404
Inquiry object:
{
"_id": "...",
"listing": { "_id": "...", "name": "Casa Verde Dormitory" },
"seeker": { "_id": "...", "firstName": "Juan", "lastName": "Dela Cruz" },
"owner": { "_id": "...", "firstName": "Maria", "lastName": "Santos" },
"messages": [
{ "_id": "...", "sender": "<seeker id>", "body": "Is water included in the rent?", "createdAt": "..." },
{ "_id": "...", "sender": "<owner id>", "body": "Yes, water is included.", "createdAt": "..." }
],
"lastMessageAt": "..."
}All endpoints here are Admin only.
All users, newest first. Filters: role, isActive, q (matches name or email).
200 → paged list of User
Deactivate or reactivate a user (D-08).
{ "isActive": false }200 → User · 400 trying to change an admin account (including your own) · 404
All listings, newest first. Same filters as GET /api/listings, plus ownerId.
200 → paged list of ListingSummary with owner as a PublicUser
To edit or delete a listing, admins use the normal PATCH /api/listings/:id and DELETE /api/listings/:id.
| Method | Path | Who | FR |
|---|---|---|---|
| POST | /api/auth/register |
Public | FR-01 |
| POST | /api/auth/login |
Public | FR-02 |
| GET | /api/users/me |
Logged in | FR-03 |
| PATCH | /api/users/me |
Logged in | FR-03 |
| PATCH | /api/users/me/password |
Logged in | FR-03 |
| GET | /api/listings |
Public | FR-07–09 |
| GET | /api/listings/mine |
Owner | FR-04 |
| GET | /api/listings/:id |
Public | FR-10 |
| POST | /api/listings |
Owner | FR-04, 05 |
| PATCH | /api/listings/:id |
Owner (own), Admin | FR-04, 17 |
| PATCH | /api/listings/:id/availability |
Owner (own) | FR-06 |
| DELETE | /api/listings/:id |
Owner (own), Admin | FR-04, 17 |
| POST | /api/listings/:id/photos |
Owner (own) | FR-05 |
| DELETE | /api/listings/:id/photos/:photoId |
Owner (own) | FR-05 |
| POST | /api/reservations |
Seeker | FR-13 |
| GET | /api/reservations/mine |
Seeker | FR-15 |
| GET | /api/reservations/incoming |
Owner | FR-14 |
| PATCH | /api/reservations/:id/accept |
Owner (own) | FR-14, 16 |
| PATCH | /api/reservations/:id/reject |
Owner (own) | FR-14 |
| DELETE | /api/reservations/:id |
Seeker (own) | FR-15 |
| POST | /api/inquiries |
Seeker | FR-11 |
| GET | /api/inquiries |
Seeker, Owner | FR-11, 12 |
| GET | /api/inquiries/:id |
Thread members | FR-11, 12 |
| POST | /api/inquiries/:id/messages |
Thread members | FR-12 |
| GET | /api/admin/users |
Admin | FR-17 |
| PATCH | /api/admin/users/:id/status |
Admin | FR-17 |
| GET | /api/admin/listings |
Admin | FR-17 |
Route order note for the back end: mount /api/listings/mine before /api/listings/:id, or Express will treat mine as an id.