The Node.js backend is an Express.js server providing platform services: user authentication, bookmarks, annotations, highlights, dossiers, and search history. It uses PostgreSQL with Prisma ORM.
Server Port: 3001
- Node.js 18+
- PostgreSQL database
cd backend
npm installCopy the example file and configure:
cp .env.example .env| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | - | PostgreSQL connection string |
JWT_SECRET |
Yes | - | Secret key for JWT signing (change in production!) |
JWT_ACCESS_EXPIRY |
No | 30m |
Access token TTL |
JWT_REFRESH_EXPIRY |
No | 7d |
Refresh token TTL |
PORT |
No | 3001 |
Server port |
NODE_ENV |
No | development |
Environment mode |
ALLOWED_ORIGINS |
No | localhost:5173,localhost:3001 |
CORS allowed origins (comma-separated) |
Example .env:
DATABASE_URL="postgresql://visualex:visualex@localhost:5432/visualex_platform"
JWT_SECRET="your-secret-key-change-in-production"
JWT_ACCESS_EXPIRY="30m"
JWT_REFRESH_EXPIRY="7d"
PORT=3001
NODE_ENV="development"
ALLOWED_ORIGINS="http://localhost:5173,http://localhost:3001"# Generate Prisma client
npm run prisma:generate
# Run migrations
npm run prisma:migrate
# (Optional) Open Prisma Studio GUI
npm run prisma:studio# Development (hot reload)
npm run dev
# Production
npm run build
npm start| Script | Description |
|---|---|
dev |
Run with tsx watch (hot reload) |
build |
Compile TypeScript to dist/ |
start |
Run compiled server |
prisma:generate |
Generate Prisma client |
prisma:migrate |
Create and run migrations |
prisma:studio |
Open Prisma Studio (port 5555) |
prisma:reset |
Reset database (destructive!) |
db:seed |
Seed database with sample data |
erDiagram
User ||--o{ Folder : owns
User ||--o{ Bookmark : owns
User ||--o{ Annotation : owns
User ||--o{ Highlight : owns
User ||--o{ Dossier : owns
User ||--o{ Feedback : submits
User ||--o{ SearchHistory : has
Folder ||--o{ Folder : contains
Folder ||--o{ Bookmark : contains
Bookmark ||--o{ Annotation : has
Bookmark ||--o{ Highlight : has
Dossier ||--o{ DossierItem : contains
User {
uuid id PK
string email UK
string username UK
string password
boolean isActive
boolean isVerified
boolean isAdmin
int loginCount
datetime lastLoginAt
datetime createdAt
datetime updatedAt
}
Folder {
uuid id PK
string name
string description
string color
string icon
int position
uuid parentId FK
uuid userId FK
}
Bookmark {
uuid id PK
string normaKey
json normaData
string title
string notes
array tags
uuid folderId FK
uuid userId FK
}
Annotation {
uuid id PK
string normaKey
string content
enum annotationType
string textContext
int position
uuid bookmarkId FK
uuid userId FK
}
Highlight {
uuid id PK
string normaKey
string text
string color
int startOffset
int endOffset
string note
uuid bookmarkId FK
uuid userId FK
}
Dossier {
uuid id PK
string name
string description
string color
uuid userId FK
}
DossierItem {
uuid id PK
uuid dossierId FK
enum itemType
string title
json content
int position
}
Feedback {
uuid id PK
enum type
string message
enum status
uuid userId FK
}
SearchHistory {
uuid id PK
string actType
string actNumber
string article
string date
string version
uuid userId FK
}
AnnotationType: note, question, important, follow_up, summary
DossierItemType: norm, note, section
FeedbackType: bug, suggestion, other
FeedbackStatus: new, read, resolved, dismissed
All endpoints are prefixed with /api.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register |
No | User registration (creates inactive user) |
| POST | /auth/login |
No | Login, returns access + refresh tokens |
| POST | /auth/refresh |
No | Refresh access token |
| GET | /auth/me |
Yes | Get current user |
| PUT | /auth/change-password |
Yes | Change password |
// Request
{
"email": "user@example.com",
"username": "johndoe",
"password": "securepassword"
}
// Response (201)
{
"id": "uuid",
"email": "user@example.com",
"username": "johndoe",
"is_active": false,
"is_admin": false
}Note: New users are created with isActive: false. An admin must activate them.
// Request
{
"email": "user@example.com",
"password": "securepassword"
}
// Response (200)
{
"access_token": "eyJhbGc...",
"refresh_token": "eyJhbGc...",
"user": {
"id": "uuid",
"email": "user@example.com",
"username": "johndoe"
}
}All admin routes require authentication and admin privileges.
| Method | Path | Description |
|---|---|---|
| GET | /admin/users |
List all users with stats |
| POST | /admin/users |
Create new user |
| GET | /admin/users/:id |
Get user details |
| PUT | /admin/users/:id |
Update user (email, username, status, admin) |
| DELETE | /admin/users/:id |
Delete user (cascades all data) |
| POST | /admin/users/:id/reset-password |
Reset user password |
| GET | /admin/feedbacks |
List all feedback |
| GET | /admin/feedbacks/stats |
Feedback statistics |
| PUT | /admin/feedbacks/:id |
Update feedback status |
| DELETE | /admin/feedbacks/:id |
Delete feedback |
| Method | Path | Description |
|---|---|---|
| POST | /bookmarks |
Create bookmark |
| GET | /bookmarks |
List bookmarks (supports ?folderId=... ?tags=...) |
| GET | /bookmarks/:id |
Get bookmark with annotations/highlights |
| PUT | /bookmarks/:id |
Update bookmark |
| PATCH | /bookmarks/:id/move |
Move to folder |
| DELETE | /bookmarks/:id |
Delete bookmark |
| POST | /bookmarks/bulk/delete |
Bulk delete |
| POST | /bookmarks/bulk/move |
Bulk move to folder |
// Request
{
"norma_key": "Art. 2043 c.c.",
"norma_data": { "tipo_atto": "codice civile", ... },
"title": "Tort Liability",
"notes": "Important for civil law exam",
"tags": ["civil", "torts"],
"folder_id": "uuid"
}| Method | Path | Description |
|---|---|---|
| POST | /folders |
Create folder |
| GET | /folders |
List folders |
| GET | /folders/tree |
Get folder hierarchy |
| GET | /folders/:id |
Get folder |
| PUT | /folders/:id |
Update folder |
| PATCH | /folders/:id/move |
Move folder (with circular reference check) |
| DELETE | /folders/:id |
Delete folder (cascades bookmarks) |
| POST | /folders/bulk/delete |
Bulk delete |
| POST | /folders/bulk/move |
Bulk move |
| Method | Path | Description |
|---|---|---|
| POST | /highlights |
Create highlight |
| GET | /highlights |
Get highlights (supports ?normaKey=...) |
| PUT | /highlights/:id |
Update highlight |
| DELETE | /highlights/:id |
Delete highlight |
{
"norma_key": "Art. 2043 c.c.",
"text": "danno ingiusto",
"color": "yellow",
"start_offset": 45,
"end_offset": 59,
"note": "Key concept"
}Colors: yellow, green, red, blue
| Method | Path | Description |
|---|---|---|
| POST | /annotations |
Create annotation |
| GET | /annotations |
Get annotations (supports ?normaKey=... ?type=...) |
| PUT | /annotations/:id |
Update annotation |
| DELETE | /annotations/:id |
Delete annotation |
Types: note, question, important, follow_up, summary
| Method | Path | Description |
|---|---|---|
| GET | /dossiers |
List all dossiers with items |
| POST | /dossiers |
Create dossier |
| GET | /dossiers/:id |
Get dossier with items |
| PUT | /dossiers/:id |
Update dossier |
| DELETE | /dossiers/:id |
Delete dossier |
| POST | /dossiers/:id/items |
Add item to dossier |
| PUT | /dossiers/:id/items/:itemId |
Update item |
| DELETE | /dossiers/:id/items/:itemId |
Delete item |
| POST | /dossiers/:id/reorder |
Reorder items |
| Method | Path | Description |
|---|---|---|
| POST | /feedback |
Submit feedback (min 10 chars) |
{
"type": "bug",
"message": "The PDF export fails when..."
}| Method | Path | Description |
|---|---|---|
| GET | /history |
List search history (supports ?limit= ?offset=) |
| POST | /history |
Add search (deduplicates within 5 minutes) |
| DELETE | /history/:id |
Delete single item |
| DELETE | /history |
Clear all history |
| Method | Path | Description |
|---|---|---|
| GET | /health |
Returns status, timestamp, environment |
sequenceDiagram
participant C as Client
participant S as Server
participant DB as Database
C->>S: POST /auth/register
S->>DB: Create user (isActive: false)
S-->>C: 201 User created
Note over C,S: Admin activates user
C->>S: POST /auth/login
S->>DB: Verify credentials
S->>DB: Update loginCount, lastLoginAt
S-->>C: 200 {access_token, refresh_token}
C->>S: GET /api/bookmarks (Authorization: Bearer <access_token>)
S->>S: Verify JWT
S->>DB: Fetch user bookmarks
S-->>C: 200 Bookmarks
Note over C,S: Access token expires (30m)
C->>S: POST /auth/refresh {refresh_token}
S->>S: Verify refresh token
S-->>C: 200 {new access_token, new refresh_token}
- Access Token: Short-lived (30m default), used for API requests
- Refresh Token: Long-lived (7d default), used to get new access tokens
- Hashing: bcryptjs with 10 salt rounds
- Minimum length: 3 characters (configurable)
All errors return JSON:
{
"detail": "Error message"
}Status Codes:
| Code | Description |
|---|---|
201 |
Resource created |
204 |
Deletion successful |
400 |
Validation error, duplicate entry |
401 |
Unauthorized (missing/invalid token) |
403 |
Forbidden (admin required) |
404 |
Resource not found |
500 |
Internal server error |
- Cannot remove own admin privileges
- Cannot deactivate own account
- Cannot delete own account
- All user data cascades on user deletion
- Folder deletion cascades to bookmarks
- Users can only access their own data
Folder move operations prevent a folder from being moved into its own descendant.
- CORS - Configurable allowed origins
- Body Parsing - JSON and URL-encoded
- Express Async Errors - Automatic promise rejection handling
- Authentication - JWT verification for protected routes
- Admin Authorization - Admin flag check for admin routes
- Error Handler - Centralized error response formatting
backend/
├── prisma/
│ └── schema.prisma # Database schema
├── src/
│ ├── index.ts # Entry point
│ ├── config.ts # Configuration
│ ├── controllers/ # Business logic
│ ├── routes/ # Route definitions
│ ├── middleware/ # Auth, admin, error handling
│ └── utils/ # JWT, helpers
├── package.json
└── tsconfig.json
Runtime:
@prisma/client- Prisma ORM clientexpress- Web frameworkbcryptjs- Password hashingjsonwebtoken- JWT tokenscors- CORS middlewaredotenv- Environment variableszod- Schema validationexpress-async-errors- Async error handling
Development:
typescript- TypeScript compilertsx- TypeScript executorprisma- Prisma CLI