This document describes the current backend implementation in this repository. It is based on the code currently in the project, with the legacy PDF used only as historical context and compared against the live implementation.
Source of truth priority used for this document:
- Current backend source code
- Current route definitions
- Current controllers
- Current models
- Current middleware
- Current server configuration
- Historical PDF
The project configuration is explicit about a local development server:
PORT=5000in.envapp.listen(PORT, ...)inindex.js- Default Express server is therefore on
http://localhost:5000
Current API base URL in this repository:
http://localhost:5000/api
The source code does not include a production hostname or deployment domain. The historical PDF mentions a hosted URL, but that is not authoritative for the current implementation unless the production environment is configured elsewhere outside this repository.
| Method | Endpoint | Authentication | Description |
|---|---|---|---|
| GET | / |
No | Public welcome/health endpoint |
| POST | /api/auth/register |
No | Register a new user |
| POST | /api/auth/login |
No | Authenticate and receive JWT |
| POST | /api/auth/logout |
No | Clear auth cookie |
| GET | /api/plans/allplans |
Yes | Get all plans for the authenticated user |
| GET | /api/plans/plan/:id |
Yes | Get a single plan by ID |
| POST | /api/plans/create |
Yes | Create a trading plan |
| PUT | /api/plans/update/:id |
Yes | Update a trading plan |
| DELETE | /api/plans/delete/:id |
Yes | Delete a trading plan |
| POST | /api/jornal/create |
Yes | Create a journal trade entry with optional images |
| GET | /api/jornal/all |
Yes | Get all journal entries for the authenticated user |
| GET | /api/jornal/jornal/:id |
Yes | Get one journal entry by ID |
| DELETE | /api/jornal/delete/:id |
Yes | Delete a journal entry |
| POST | /api/jornal/delete |
Yes | Delete journal fallback route |
| PUT | /api/jornal/update/:id |
Yes | Update a journal entry |
| POST | /api/jornal/update |
Yes | Update journal fallback route |
| GET | /api/analytics |
Yes | Get analytics metrics |
| PUT | /api/user/profile |
Yes | Update authenticated user profile and avatar |
| GET | /upload/* |
No | Static file serving for uploaded images |
Authentication is implemented with JWT and a cookie fallback.
The auth controller creates a JWT with:
jwt.sign({ id: userId, email }, process.env.JWT_SECRET, {
expiresIn: "1d",
});The token is signed using process.env.JWT_SECRET and expires in 1 day.
The server supports this header:
Authorization: Bearer <JWT_TOKEN>The middleware also checks req.cookies.token first, then falls back to the Authorization header.
The protect middleware is used on all routes that require login. It performs the following:
- Reads the cookie
token. - If no cookie is present, checks
Authorization: Bearer .... - Calls
jwt.verify(token, process.env.JWT_SECRET). - Finds the user by
decoded.idand attaches it toreq.user. - Returns
401if there is no token, the token fails verification, or the user no longer exists.
{ "success": false, "message": "Not authorized, token missing" }{ "success": false, "message": "Not authorized, token failed" }{ "success": false, "message": "User no longer exists" }- Login and registration both set the
tokencookie ashttpOnly. - The JSON response also includes
tokenfor convenience. - Protected routes reject unauthenticated requests with HTTP 401.
logoutclears the cookie but does not invalidate the JWT server-side.
The application entry is index.js.
It configures:
helmet()security headerscors()- JSON parser with
express.json({ limit: "10mb" }) - static file hosting at
/upload - route mounting:
/api/auth/api/plans/api/jornal/api/analytics/api/user
Example welcome route:
GET /Response:
{
"success": true,
"message": "Welcome to ApexTrade — Trading Journal API 🚀",
"version": "1.0.0",
"description": "The backend engine for tracking trading plans, multi-timeframe journals, and performance analytics.",
"status": "Active & Secure"
}File: models/User.js
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
String | Yes | - | Required; no trim in schema |
email |
String | Yes | - | Unique; email is not validated in schema |
password |
String | Yes | - | Stored as hashed text |
avatar |
String | No | "" |
URL/path to avatar image |
Example object:
{
"_id": "64...",
"name": "John Doe",
"email": "john@example.com",
"password": "hashedPassword",
"avatar": "/upload/avatars/avatar-1720000000000.png"
}File: models/Plan.js
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
String | Yes | - | min 2, max 100, trimmed |
description |
String | No | "" |
max 1000 chars |
rules |
String[] | Yes | [] |
every item must be a non-empty string; max 500 chars each |
image.data |
String | No | null |
Base64-like payload or string value |
image.mimeType |
String | No | null |
Example: image/jpeg |
createdAt |
Date | Auto | - | Added by timestamps |
updatedAt |
Date | Auto | - | Added by timestamps |
File: models/jornalModel.js
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
user |
ObjectId | Yes | - | Ref to User |
plan |
ObjectId | No | null |
Ref to Plan |
asset |
String | Yes | - | trimmed and uppercased |
type |
String | Yes | - | enum includes LONG, SHORT, BUY, SELL |
entryPrice |
Number | Yes | - | min 0 |
stopLoss |
Number | No | 0 |
min 0 |
takeProfit |
Number | No | 0 |
min 0 |
exitPrice |
Number | Yes | - | min 0 |
lotSize |
Number | Yes | - | min 0 |
risk |
Number | No | 0 |
min 0 |
pnl |
Number | Yes | - | required contribution to P&L |
result |
String | No | "Break Even" |
enum Win, Loss, Break Even |
setup |
String | No | "" |
trimmed |
emotion |
String | No | "CALM" |
enum of emotion values |
afterEmotion |
String | No | "" |
enum includes empty string |
notes |
String | No | "" |
trimmed |
highTimeFrameImage |
String | No | null |
public path |
mediumTimeFrameImage |
String | No | null |
public path |
lowTimeFrameImage |
String | No | null |
public path |
date |
Date | No | Date.now |
trade date |
createdAt |
Date | Auto | - | timestamp |
updatedAt |
Date | Auto | - | timestamp |
The schema allows:
enum: ["LONG", "SHORT", "BUY", "SELL"];But the controller validation for journal creation only accepts:
["LONG", "SHORT"];So the current API behavior is stricter than the schema definition.
The server exposes uploaded files under /upload:
app.use("/upload", express.static("upload"));This means uploaded files are available via a URL like:
http://localhost:5000/upload/avatars/<filename>
http://localhost:5000/upload/journals/<filename>
The profile upload code stores avatars as:
`/upload/avatars/${req.file.filename}`;The journal upload code stores images as:
`/upload/journals/${req.files.<field>[0].filename}`The file names are generated by the Multer storage logic.
Creates a user account and returns a JWT token and user payload.
Not required.
| Header | Required | Value |
|---|---|---|
Content-Type |
Yes | application/json |
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Visible user name |
email |
string | Yes | Must be a valid email format |
password |
string | Yes | Min 6 characters |
name,email, andpasswordmust all be presentemailmust match/^[^\s@]+@[^\s@]+\.[^\s@]+$/password.length >= 6- duplicate email will return 400
HTTP 201
{
"success": true,
"message": "Account created successfully",
"token": "eyJ...",
"user": {
"id": "64...",
"name": "John Doe",
"email": "john@example.com"
}
}The response also sets an httpOnly cookie named token.
- 400: missing fields
- 400: invalid email
- 400: password too short
- 400: user already exists
- 500: server failure
Example error:
{
"success": false,
"error": "User already exists with this email."
}curl -X POST http://localhost:5000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "securepassword123"
}'Authenticates an existing user and returns a JWT.
Not required.
| Header | Required | Value |
|---|---|---|
Content-Type |
Yes | application/json |
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Registered email |
password |
string | Yes | User password |
- both fields must be present
- email is trimmed and lowercased
- password must match stored hash
HTTP 200
{
"success": true,
"message": "Logged in successfully",
"token": "eyJ...",
"user": {
"id": "64...",
"name": "John Doe",
"email": "john@example.com"
}
}The response also sets the token cookie.
- 400: missing email or password
- 400: invalid credentials
- 500: server failure
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "securepassword123"
}'Clears the token cookie.
Not required by the route itself.
No required headers.
No body.
HTTP 200
{
"success": true,
"message": "Logged out successfully"
}- 500: server failure
curl -X POST http://localhost:5000/api/auth/logoutReturns all plans in descending creation order.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
No body.
HTTP 200
{
"success": true,
"count": 1,
"plans": [
{
"_id": "64...",
"name": "Breakout Strategy",
"description": "Daily morning session breakout strategy.",
"rules": [
"Wait for 15-minute candle close",
"Ensure high relative volume"
],
"image": {
"data": null,
"mimeType": null
},
"createdAt": "2026-06-06T12:00:00.000Z",
"updatedAt": "2026-06-06T12:00:00.000Z"
}
]
}- 401: missing or invalid token
- 500: server error
Fetches one trading plan by MongoDB ObjectId.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | MongoDB ObjectId of the plan |
- Must be a valid MongoDB ObjectId.
- Returns 400 if
idis not valid.
HTTP 200
{
"success": true,
"plan": {
"_id": "64...",
"name": "Breakout Strategy",
"description": "Daily morning session breakout strategy.",
"rules": ["Wait for 15-minute candle close", "Ensure high relative volume"],
"image": {
"data": null,
"mimeType": null
},
"createdAt": "2026-06-06T12:00:00.000Z",
"updatedAt": "2026-06-06T12:00:00.000Z"
}
}- 400: invalid plan ID format
- 401: missing/invalid token
- 404: plan not found
- 500: server error
Creates a trading plan.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
Content-Type |
Yes | application/json |
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Plan name; must be non-empty after trim |
description |
string | No | Description text |
rules |
string[] | Yes | Must be an array |
image |
object | No | Optional plan image object |
image.data |
string | Conditional | Must be provided with mimeType when image exists |
image.mimeType |
string | Conditional | Must be provided with data |
nameis required and trimmedrulesmust be an array- empty strings in
rulesare removed - if
imageis provided, bothimage.dataandimage.mimeTypemust exist
HTTP 201
{
"success": true,
"message": "Trading plan created successfully",
"plan": {
"_id": "64...",
"name": "Trend Following",
"description": "Trading with the 200 EMA trend on 1H chart.",
"rules": ["Price must be above 200 EMA", "Wait for pullback to support"],
"image": {
"data": "base64_string_here...",
"mimeType": "image/jpeg"
},
"createdAt": "2026-06-06T12:15:00.000Z",
"updatedAt": "2026-06-06T12:15:00.000Z"
}
}- 400: missing name
- 400: rules must be an array
- 400: image missing required fields
- 401: missing/invalid token
- 500: server error
curl -X POST http://localhost:5000/api/plans/create \
-H "Authorization: Bearer <JWT_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "Trend Following",
"description": "Trading with the 200 EMA trend on 1H chart.",
"rules": [
"Price must be above 200 EMA",
"Wait for pullback to support"
],
"image": {
"data": "base64_string_here...",
"mimeType": "image/jpeg"
}
}'Updates an existing plan. All fields are optional in the request body.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
Content-Type |
Yes | application/json |
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | MongoDB ObjectId |
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | New plan name |
description |
string | No | New description |
rules |
string[] | No | Replacement rule list |
image |
object | No | Set to null to clear image or provide new object |
idmust be valid MongoDB ObjectId- if
nameis present, it cannot be empty after trim - if
rulesis present, it must be an array - if
imageis present and notnull, bothdataandmimeTypeare required
HTTP 200
{
"success": true,
"message": "Trading plan updated successfully",
"plan": {
"_id": "64...",
"name": "Updated Trend Following Plan",
"description": "Updated description",
"rules": ["New rule added"],
"image": {
"data": "base64_string_here...",
"mimeType": "image/jpeg"
}
}
}- 400: invalid plan ID format
- 400: empty plan name
- 400: rules not array
- 400: invalid image object
- 401: missing/invalid token
- 404: plan not found
- 500: server error
Permanently deletes a trading plan.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | MongoDB ObjectId |
HTTP 200
{
"success": true,
"message": "Trading plan deleted successfully"
}- 400: invalid plan ID format
- 401: missing/invalid token
- 404: plan not found
- 500: server error
Creates a journal record for a trading setup. It supports an optional set of uploaded charts for each timeframe.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
Content-Type |
Yes for JSON request | application/json |
Content-Type |
Yes for multipart request | multipart/form-data |
This endpoint accepts either JSON or multipart form data.
| Field | Type | Required | Description |
|---|---|---|---|
asset |
string | Yes | Market symbol or asset code |
type |
string | Yes | Must be LONG or SHORT in controller validation |
pnl |
number | Yes | Profit/loss amount |
entryPrice |
number | Yes | Entry price |
stopLoss |
number | No | Default 0 if empty |
takeProfit |
number | No | Default 0 if empty |
exitPrice |
number | Yes | Exit price |
lotSize |
number | Yes | Trade size or lots |
risk |
number | No | Default 0 if empty |
result |
string | No | Must be Win, Loss, or Break Even |
setup |
string | No | Setup description |
emotion |
string | No | Allowed enum values listed below |
afterEmotion |
string | No | Optional follow-up emotion |
notes |
string | No | Additional notes |
date |
date/string | No | Date value; defaults to Date.now() |
plan |
string/ObjectId | No | Optional associated plan reference |
| Field | Type | Required | Description |
|---|---|---|---|
asset |
string | Yes | Asset code |
type |
string | Yes | LONG or SHORT |
pnl |
number | Yes | Profit/loss value |
entryPrice |
number | Yes | Entry price |
stopLoss |
number | No | Optional |
takeProfit |
number | No | Optional |
exitPrice |
number | Yes | Exit price |
lotSize |
number | Yes | Lot size |
risk |
number | No | Optional |
result |
string | No | Win, Loss, Break Even |
setup |
string | No | Optional text |
emotion |
string | No | One of the allowed emotion values |
afterEmotion |
string | No | Optional |
notes |
string | No | Optional text |
date |
string | No | Optional date |
plan |
string | No | Optional plan id |
highTimeFrameImage |
file | No | Up to 1 image, JPEG/PNG/WEBP, max 5MB |
mediumTimeFrameImage |
file | No | Up to 1 image, JPEG/PNG/WEBP, max 5MB |
lowTimeFrameImage |
file | No | Up to 1 image, JPEG/PNG/WEBP, max 5MB |
The controller performs these checks:
assetandtypeare requiredtypemust beLONGorSHORTpnlis requiredentryPriceis requiredexitPriceis requiredlotSizeis required- numeric conversion is required for
pnl,entryPrice,exitPrice,lotSize,stopLoss,takeProfit,risk - if
resultis present, it must be one of:Win,Loss,Break Even emotionis defaulted toCALMif invalid or undefined- uploaded files are deleted on validation failure
The controller accepts these values when provided:
FOCUSED
CONFIDENT
DISCIPLINED
CALM
ANXIOUS
UNCERTAIN
FEARFUL
GREEDY
FRUSTRATED
IMPULSIVE
FOMO
REVENGE
The schema also allows those exact values, and the default is CALM.
HTTP 201
{
"success": true,
"message": "Journal entry created successfully.",
"data": {
"_id": "64...",
"user": "64...",
"plan": null,
"asset": "EURUSD",
"type": "LONG",
"pnl": 50,
"entryPrice": 1.05,
"stopLoss": 1.045,
"takeProfit": 1.06,
"exitPrice": 1.055,
"lotSize": 1,
"risk": 0.5,
"result": "Win",
"setup": "Breakout",
"emotion": "CONFIDENT",
"afterEmotion": "",
"notes": "Good trade",
"highTimeFrameImage": "/upload/journals/high-<userid>-<timestamp>.png",
"mediumTimeFrameImage": null,
"lowTimeFrameImage": null,
"date": "2026-06-06T12:00:00.000Z"
}
}- 400: missing asset or type
- 400: trade type not LONG/SHORT
- 400: missing P&L
- 400: missing entry price
- 400: missing exit price
- 400: missing lot size
- 400: invalid numeric values
- 400: invalid
result - 401: missing/invalid token
- 500: server failure
curl -X POST http://localhost:5000/api/jornal/create \
-H "Authorization: Bearer <JWT_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"asset": "EURUSD",
"type": "LONG",
"entryPrice": 1.05,
"exitPrice": 1.055,
"stopLoss": 1.045,
"takeProfit": 1.06,
"lotSize": 1,
"pnl": 50,
"risk": 0.5,
"result": "Win",
"setup": "Breakout",
"emotion": "CONFIDENT",
"notes": "Good trade"
}'curl -X POST http://localhost:5000/api/jornal/create \
-H "Authorization: Bearer <JWT_TOKEN>" \
-F "asset=GBPUSD" \
-F "type=SHORT" \
-F "entryPrice=1.2700" \
-F "exitPrice=1.2650" \
-F "stopLoss=1.2750" \
-F "takeProfit=1.2600" \
-F "lotSize=2.0" \
-F "pnl=100" \
-F "risk=1.0" \
-F "result=Win" \
-F "setup=Support Bounce" \
-F "emotion=FOCUSED" \
-F "notes=Perfect risk reward" \
-F "highTimeFrameImage=@/tmp/high.png" \
-F "mediumTimeFrameImage=@/tmp/medium.png" \
-F "lowTimeFrameImage=@/tmp/low.png"Fetches all journal entries for the authenticated user, sorted by newest first.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
HTTP 200
{
"success": true,
"count": 2,
"stats": {
"totalPnl": 150,
"winRate": "50.00%",
"winningTrades": 1,
"losingTrades": 1,
"breakEvenTrades": 0
},
"data": [
{
"_id": "64...",
"user": "64...",
"plan": null,
"asset": "EURUSD",
"type": "LONG",
"pnl": 50,
"entryPrice": 1.05,
"exitPrice": 1.055,
"lotSize": 1,
"risk": 0.5,
"result": "Win",
"setup": "Breakout",
"emotion": "CONFIDENT",
"notes": "Good trade",
"date": "2026-06-06T12:00:00.000Z"
}
]
}The controller computes:
totalPnl: sum of allpnlwinningTrades:pnl > 0losingTrades:pnl < 0breakEvenTrades:pnl === 0winRate:(winningTrades / totalTrades) * 100with two decimal places and a%symbol
- 401: missing/invalid token
- 500: server error
Fetches one journal entry by ID for the authenticated user.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | MongoDB ObjectId of the journal record |
HTTP 200
{
"success": true,
"data": {
"_id": "64...",
"user": "64...",
"plan": {
"_id": "64...",
"name": "Trend Following",
"description": "..."
},
"asset": "EURUSD",
"type": "LONG",
"pnl": 50,
"entryPrice": 1.05,
"stopLoss": 1.045,
"takeProfit": 1.06,
"exitPrice": 1.055,
"lotSize": 1,
"risk": 0.5,
"result": "Win",
"setup": "Breakout",
"emotion": "CONFIDENT",
"afterEmotion": "",
"notes": "Good trade",
"highTimeFrameImage": "/upload/journals/high-xxx.png",
"mediumTimeFrameImage": null,
"lowTimeFrameImage": null,
"date": "2026-06-06T12:00:00.000Z"
}
}- 401: missing/invalid token
- 404: journal not found
- 500: server error
Deletes a specific journal entry for the authenticated user.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Journal record id |
HTTP 200
{
"success": true,
"message": "Journal entry deleted successfully."
}- 401: missing/invalid token
- 404: journal not found or unauthorized
- 500: server error
The route also keeps a fallback POST route at /api/jornal/delete that calls the same controller.
Fallback delete route, same functionality as DELETE /api/jornal/delete/:id.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Journal record id |
Same as DELETE /api/jornal/delete/:id.
- 401: missing/invalid token
- 404: journal not found or unauthorized
- 500: server error
Updates a journal record for the authenticated user.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
Content-Type |
Yes | application/json |
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Journal record id |
The controller copies the entire request body to an updates object and applies no explicit whitelist. It removes the following keys before saving:
user_id__v
Any remaining fields are updated through MongoDB with runValidators: true.
This means the request can contain many journaling fields, but the code does not add explicit validation for each field before update. The route does not process files; file updates are not handled here.
HTTP 200
{
"success": true,
"message": "Journal entry updated successfully.",
"data": {
"_id": "64...",
"asset": "EURUSD",
"pnl": 75,
"type": "LONG"
}
}- 401: missing/invalid token
- 404: journal not found or unauthorized
- 500: server error
The route also keeps a fallback POST route at /api/jornal/update that calls the same controller.
Fallback update route, same functionality as PUT /api/jornal/update/:id.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
Content-Type |
Yes | application/json |
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Journal record id |
| ... | any | No | Any other journal field accepted by the underlying update logic |
Same as PUT /api/jornal/update/:id.
- 401: missing/invalid token
- 404: journal not found or unauthorized
- 500: server error
The current implementation uses multer with filesystem storage and multipart form uploads.
routes/jornalRoute.js attaches:
journalUpload.fields([
{ name: "highTimeFrameImage", maxCount: 1 },
{ name: "mediumTimeFrameImage", maxCount: 1 },
{ name: "lowTimeFrameImage", maxCount: 1 },
]);The upload directory is:
<project-root>/upload/journals
The filename is generated as:
<fieldname without Image>-<userId>-<timestamp><ext>
Example:
high-64a7f2...-1710000000000.png
Only these MIME types are allowed:
image/jpegimage/pngimage/webp
Each uploaded file must be 5 MB or less.
fileSize: 5 * 1024 * 1024;If a file is rejected, Multer returns an error such as:
{
"success": false,
"error": "Only JPEG, PNG, and WEBP image files are allowed."
}or
{ "success": false, "error": "Avatar file must be 5MB or less." }for profile uploads.
The controller saves the image URL as:
`/upload/journals/${req.files.highTimeFrameImage[0].filename}`;The exact published URL is built as:
http://localhost:5000/upload/journals/<filename>
This is not a Base64 API. The upload is real multipart file upload to the filesystem, and the database stores the URL path.
Updates a logged-in user profile. It supports name, email, and avatar updates.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
Content-Type |
Yes | multipart/form-data |
No URL params.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | New user name |
email |
string | No | New email address |
avatar |
file | No | Avatar image upload |
- At least one of
name,email, oravatarmust be present - if
nameis provided, it cannot be empty after trim - if
emailis provided, it cannot be empty and must match the same email regex - if the new email is already used by another user, return 400
- avatar upload is limited to 5 MB
- allowed avatar MIME types:
image/jpeg,image/png,image/webp
The route uses:
upload.single("avatar");The route is implemented in routes/userRoutes.js with a custom Multer error wrapper.
The storage directory is:
<project-root>/upload/avatars
The avatar path saved to the user document is:
`/upload/avatars/${req.file.filename}`;HTTP 200
{
"success": true,
"message": "Profile updated successfully",
"user": {
"id": "64...",
"name": "New Name",
"email": "new@example.com",
"avatar": "/upload/avatars/avatar-1710000000000.png"
}
}- 400: no name/email/avatar provided
- 400: name empty
- 400: email empty
- 400: invalid email format
- 400: duplicate email for another user
- 400: avatar exceeds 5MB
- 400: invalid avatar MIME type
- 401: missing/invalid token
- 404: user not found
- 500: server error
curl -X PUT http://localhost:5000/api/user/profile \
-H "Authorization: Bearer <JWT_TOKEN>" \
-F "name=Jane Doe" \
-F "email=jane@example.com" \
-F "avatar=@/tmp/avatar.png"Computes aggregated trading metrics for the authenticated user from journal records.
Required.
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer <JWT_TOKEN> |
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate |
string | No | ISO date used as lower bound |
endDate |
string | No | ISO date used as upper bound |
month |
number | No | Month number for monthly filter |
year |
number | No | Year for monthly filter |
- If both
startDateandendDateare provided, the query uses a date range. - Else if both
monthandyearare provided, the query uses a month window. - Otherwise, it uses the authenticated user and no date filter.
HTTP 200
{
"success": true,
"metrics": {
"winRate": "50.0%",
"profitFactor": "2.50",
"avgPnl": "$25.50",
"totalTrades": 10,
"winningTrades": 5,
"losingTrades": 5,
"bestTrade": "$100.00"
}
}The controller calculates:
winningTrades: number of records withpnl > 0losingTrades: number of records withpnl < 0grossProfit: sum of positivepnlgrossLoss: sum of absolute values of negativepnlwinRate:(winningTrades / totalTrades) * 100formatted with one decimal place and%profitFactor:grossProfit / grossLoss, or"Infinite"ifgrossLoss === 0andgrossProfit > 0, else"0.00"totalPnl: sum of allpnlavgPnl:(totalPnl / totalTrades).toFixed(2)prefixed with$bestTrade: maximumpnlvalue prefixed with$
If no trades match the filters, the endpoint returns:
{
"success": true,
"metrics": {
"winRate": "0%",
"profitFactor": "0.00",
"avgPnl": "$0.00",
"totalTrades": 0,
"winningTrades": 0,
"losingTrades": 0,
"bestTrade": 0
}
}- 401: missing/invalid token
- 500: server error
curl -X GET "http://localhost:5000/api/analytics?startDate=2026-01-01&endDate=2026-01-31" \
-H "Authorization: Bearer <JWT_TOKEN>"The source files define these routes exactly:
GET /POST /api/auth/registerPOST /api/auth/loginPOST /api/auth/logoutGET /api/plans/allplansGET /api/plans/plan/:idPOST /api/plans/createPUT /api/plans/update/:idDELETE /api/plans/delete/:idPOST /api/jornal/createGET /api/jornal/allGET /api/jornal/jornal/:idDELETE /api/jornal/delete/:idPOST /api/jornal/deletePUT /api/jornal/update/:idPOST /api/jornal/updateGET /api/analyticsPUT /api/user/profile
GET /upload/*fromexpress.static("upload")
- Old documentation:
http://localhost:5000/api - Current implementation: the code sets
PORT=5000and starts the Express app on that port, so the current project base URL is alsohttp://localhost:5000/api - Impact: The PDF is historically useful but the current repository authoritatively defines the local default URL, not a remote deployment domain.
- Old PDF often describes a more standard pattern like
GET /api/jornal/:idor route names that may not match the implementation exactly. - Current implementation uses:
GET /api/jornal/jornal/:idDELETE /api/jornal/delete/:idPOST /api/jornal/deletePUT /api/jornal/update/:idPOST /api/jornal/update
- Impact: The live code includes fallback POST routes and the specific route namespace
/jornal/jornal/:idis the current implementation.
- Model:
typeincludesLONG,SHORT,BUY,SELL - Controller: only accepts
LONGorSHORT - Impact: The current API rejects
BUYandSELLeven though the schema allows them. The code’s runtime validation is the authoritative behavior.
- The PDF is historical and may describe older conventions or assumptions.
- Current code uses
multer.diskStorageand stores files underupload/journalsandupload/avatars. - Impact: The current API is a real multipart upload system, not a purely JSON or Base64-only interface.
- The code uses
upload.single("avatar")withmultipart/form-data. - Impact: The API requires
avatarin the multipart form, not a JSONavatarstring payload.
The app sets cookies with:
res.cookie("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "none" : "strict",
maxAge: 24 * 60 * 60 * 1000,
});This is relevant for browser clients, but the API also supports the Authorization header directly.
Most controllers return a JSON object like:
{ "success": false, "message": "..." }or
{ "success": false, "error": "..." }The exact property names depend on the controller. Do not assume one pattern across all endpoints.
The repository does not include a centralized validation middleware or global error handler. Validation and error responses are implemented explicitly in each controller.
- Endpoints documented: 18 routes plus static upload path
- Models documented: 3
- Authentication mechanism: JWT via
jsonwebtoken, checked inprotect.js, with cookie fallback andAuthorization: Bearer ...support - Upload mechanism: Multer filesystem uploads for avatars and journal images
- API base URL:
http://localhost:5000/api - Major differences from the old PDF: route namespace details, upload approach, strict controller validation for journal
type, profile avatar field handling, and the fact that the current code should be treated as the source of truth - Anything not fully verifiable from source code: no production deployment hostname beyond the local default
PORT=5000; no external real-world API domain was configured in this repo