DevMatch is a full-stack developer networking platform that helps developers discover one another, create professional profiles, send connection requests, and chat in real time after connecting. This repository contains its backend API and Socket.IO server.
Built with Node.js, Express, TypeScript, MongoDB, and Mongoose, the application uses JWT-based, cookie-authenticated sessions to protect profiles, connections, and chats.
- Create and manage developer profiles with skills, bio, photo, and personal details.
- Browse a paginated developer feed and send or review connection requests.
- View accepted connections and pending incoming requests.
- Exchange real-time messages with accepted connections using Socket.IO.
- Persist chat history in MongoDB and retrieve it through an authenticated REST endpoint.
| Concern | Choice |
|---|---|
| Runtime | Node.js (ESM, "type": "module") |
| Language | TypeScript (strict, module: nodenext) |
| Framework | Express 5 |
| Database | MongoDB via Mongoose |
| Auth | JWT stored in an httpOnly cookie |
| Password hashing | bcrypt (10 rounds) |
| Validation | validator + custom helpers |
| CORS | cors, locked to http://localhost:5173 |
| Real-time chat | Socket.IO with authenticated, connection-only rooms |
src/
├── server.ts # Entry point — connects to DB, starts HTTP + Socket.IO server
├── app.ts # Express app — CORS, JSON, cookies, router mounting
├── config/
│ └── db.ts # Mongoose connection (exits process on failure)
├── models/
│ ├── userModel.ts # User schema + getJWTToken() / isPasswordValid()
│ ├── connectionRequest.ts # ConnectionRequest schema + Status enum
│ └── chatModel.ts # Chat participants and persisted messages
├── middlewares/
│ └── authMiddleware.ts # Reads token cookie, verifies JWT, attaches req.user
├── routes/
│ ├── authRoutes.ts # /signup, /login, /logout
│ ├── chatRoutes.ts # /chat/:targetUserId
│ ├── profileRoutes.ts # /profile/view, /profile/edit, /profile/update
│ ├── connectionRequestRoutes.ts # /request/send/..., /request/review/...
│ └── userRoutes.ts # /feed, /user/connections, /user/requests/received, /allusers
├── utils/
│ ├── socket.ts # JWT-authenticated Socket.IO chat events
│ └── validation.ts # Signup / edit / password validation helpers
└── types/express/index.d.ts # Augments Express.Request with `user`
All routers are mounted at the root path (app.use("/", ...)), so there is no /api
prefix — routes are exactly as written below.
- Node.js 20+
- A MongoDB instance (local or Atlas)
npm installCreate a .env file in the project root:
PORT=5000
MONGO_URI=mongodb://localhost:27017/gitmatch
JWT_SECRET=your-long-random-secret
NODE_ENV=developmentPORT defaults to 5000 if omitted. NODE_ENV=production makes the auth cookie secure.
npm run dev # nodemon + ts-node, watches src/
npm run build # tsc -> dist/
npm start # node dist/server.js
npm run typecheck # tsc --noEmit| Field | Type | Notes |
|---|---|---|
firstName |
string | required, max 25 chars |
lastName |
string | required at signup (validation layer) |
email |
string | required, unique, lowercased, trimmed, must be a valid email |
password |
string | required, must pass validator.isStrongPassword, stored hashed |
age |
number | minimum 18 |
gender |
string | one of male, female, others |
photoURL |
string | must be a valid URL |
about |
string | max 200 chars (enforced on edit) |
skills |
string[] | at most 10 entries |
Timestamps (createdAt, updatedAt) are enabled.
Instance methods:
getJWTToken()— signs{ _id }withJWT_SECRET.isPasswordValid(plaintext)— bcrypt compare against the stored hash.
| Field | Type | Notes |
|---|---|---|
fromUserId |
ObjectId → User |
required |
toUserId |
ObjectId → User |
required |
status |
enum | interested | ignore | accepted | rejected, defaults to interested |
Indexed on { fromUserId, toUserId }. Timestamps enabled.
Status semantics
interested/ignore— set by the sender when creating a request.accepted/rejected— set by the receiver when reviewing a request.
Each chat stores the two participant IDs and their messages. A message includes the sender ID, sender's first name, message text, and timestamp. Chat records are only created for users with an accepted connection.
POST /login sets an httpOnly cookie named token containing a JWT. Every protected
route runs authMiddleware, which:
- Reads
req.cookies.token - Verifies it with
JWT_SECRET - Loads the user document and assigns it to
req.user - Responds
401 { "message": "Unauthorized" }on any failure
Clients must send requests with credentials included (e.g. fetch(..., { credentials: "include" })),
and must originate from http://localhost:5173 to satisfy the CORS config.
DevMatch provides real-time, one-to-one messaging through Socket.IO. Socket connections
are authenticated with the same JWT cookie used by the REST API. Only users with an
accepted connection can join a shared chat room, send messages, or access the chat
history. Messages are broadcast as newMessage events and persisted in MongoDB.
| Event | Purpose |
|---|---|
joinChat |
Joins the authenticated user's room with an accepted connection. |
sendMessage |
Stores a message and broadcasts it to everyone in that chat room. |
newMessage |
Delivers a newly sent message to connected chat participants. |
chatError |
Reports an attempt to send a message before joining the room. |
Legend: 🔓 public · 🔒 requires auth cookie
Create a new account. The password is hashed before being stored.
Body:
{
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"password": "Str0ng!Pass1"
}Validation: first and last name required and alphabetic only; email must be valid;
password must be "strong" per validator.isStrongPassword.
200—user saved successfully.(plain text)500—Error : <message>(plain text)
Body:
{ "email": "ada@example.com", "password": "Str0ng!Pass1" }On success sets the token cookie and returns:
{ "message": "user logged in successfully", "user": { "...": "full user document" } }200— logged in, cookie set500—Error : Invalid Credentials./Error : Email address not valid.
Clears the token cookie by setting it to an already-expired value.
200—Logged out successfully(plain text)
Returns the authenticated user's document.
{ "user": { "_id": "...", "firstName": "Ada", "email": "ada@example.com", "...": "..." } }200— user object401— not authenticated
Updates editable profile fields. Only these keys are accepted — any other key in the body rejects the entire request:
firstName, lastName, age, gender, photoURL, about, skills
Additional rules: about ≤ 200 characters, photoURL must be a valid URL.
Body (all fields optional):
{
"firstName": "Ada",
"age": 28,
"gender": "female",
"photoURL": "https://example.com/ada.png",
"about": "Systems programmer who likes long compiles.",
"skills": ["typescript", "rust", "mongodb"]
}200—{ "message": "Profile updated successfully", "user": { ... } }400—Error : Edit request not valid.(plain text)401— not authenticated
Changes the account password.
Body:
{
"currentPassword": "Str0ng!Pass1",
"oldPassword": "Str0ng!Pass1",
"newPassword": "Even$tr0nger2"
}currentPassword is bcrypt-compared against the stored hash. newPassword is required
and must differ from oldPassword. On success the hash is replaced.
200—Password updated successfully.(plain text)400—Error : Current password is not valid./Error : New password req/Error : New Password Invalid.401— not authenticated
Send a connection request to another user.
| Param | Value |
|---|---|
:status |
interested or ignore |
:toUserId |
a valid MongoDB ObjectId |
Rejected when: the status isn't one of the two allowed values, toUserId isn't a valid
ObjectId, the target user doesn't exist, you send to yourself, or a request already exists
in either direction between the two users.
Example: POST /request/send/interested/6721b0f4c1a2d3e4f5a6b7c8
200—{ "message": "Connection request sent successfully", "data": { ... } }400—Error : Connection req already sent./Error : Invalid status for connection request.(plain text)404—{ "message": "Invalid userid." }/{ "message": "User not found." }401— not authenticated
Accept or reject a pending request addressed to you. Only matches requests where
toUserId is the logged-in user and the current status is interested.
| Param | Value |
|---|---|
:status |
accepted or rejected |
:requestId |
the ConnectionRequest _id |
Example: POST /request/review/accepted/6721b1a9c1a2d3e4f5a6b7d0
200—{ "message": "Connection request accepted", "data": { ... } }401—{ "error": "Invalid request status type." }/{ "error": "Connection request not found." }/ unauthenticated
Pending requests sent to you (status interested), with the sender populated using
the safe field set (firstName lastName photoURL age gender about skills).
{
"message": "You've got 2 connection request(s).",
"connectionRequests": [
{
"_id": "...",
"fromUserId": { "_id": "...", "firstName": "Grace", "skills": ["go"] },
"toUserId": "...",
"status": "interested"
}
]
}200— as above401— not authenticated501—{ "error": "<message>" }
All users you are connected with — i.e. requests in either direction with status
accepted. Returns a flat array of the other party's safe fields.
[
{ "_id": "...", "firstName": "Grace", "lastName": "Hopper", "skills": ["cobol"] }
]200— array of users400—{ "error": "<message>" }401— not authenticated
Paginated list of candidate users to swipe on. Excludes yourself and everyone you already
have a connection request with (in either direction). If your profile has a gender set,
users of the same gender are filtered out.
Query parameters:
| Param | Default | Notes |
|---|---|---|
page |
1 |
1-indexed |
limit |
10 |
capped at 50 |
Example: GET /feed?page=2&limit=20
Returns an array of users with the safe field set, sorted by _id ascending.
200— array of users400—{ "message": "<message>" }401— not authenticated
Returns the authenticated user's chat history with an accepted connection. If no chat exists yet, an empty chat is created. Both the REST endpoint and Socket.IO chat rooms are restricted to accepted connections.
| Param | Value |
|---|---|
:targetUserId |
A valid MongoDB ObjectId for an accepted connection |
200—{ "chat": { "participants": [...], "messages": [...] } }400—{ "message": "Invalid target user ID" }403—{ "message": "Not allowed" }401— not authenticated
Returns every user document in the collection. Intended as a development/debugging helper — see the note below.
200— array of full user documents400—{ "message": "<message>" }
Responses are not fully uniform yet — some handlers send plain text, others JSON:
- Plain text
Error : <message>— signup, login, logout, profile routes,POST /request/send - JSON
{ "error": "<message>" }—POST /request/review,/user/* - JSON
{ "message": "<message>" }—/feed,/allusers, auth failures
Standardizing on a single JSON error shape is a good next step.
These are accurate descriptions of the current code, not aspirational docs:
GET /allusersis unauthenticated and returns password hashes along with every other field. It should be removed or put behind auth + the safe projection before deploying.PATCH /profile/updatereads three password fields.currentPasswordis verified against the hash, whileoldPasswordis only used for the "must be different" check — so a client that omitsoldPasswordpasses that check trivially.validateNewPasswordoverwrites its own result: the strong-password check is computed and then discarded by the following assignment, so only the "different fromoldPassword" condition actually applies.POST /logoutsets the cookie to the stringnullrather than clearing it; the immediate expiry is what ends the session.- The
{ fromUserId, toUserId }index is not unique, so duplicate-request prevention relies solely on the application-level lookup and can race under concurrency. - The self-request guard in the ConnectionRequest schema is commented out; the route handler performs that check instead.
startServerswallows errors — itscatchblock is empty, so a failure after the DB connect step exits silently.- CORS origin is hardcoded to
http://localhost:5173; make it configurable per environment before deploying.
ISC — author: Sachin