REST API backend for the Open Path learning platform. Built with Express.js and Prisma ORM on PostgreSQL.
- Runtime: Node.js (CommonJS)
- Framework: Express v5
- ORM: Prisma with
@prisma/adapter-pg - Database: PostgreSQL
- Auth: JWT (access + refresh tokens) with bcryptjs
- Validation: Joi
- File Storage: S3-compatible (via
multer-s3) - Push Notifications: Firebase Cloud Messaging (FCM)
- Package Manager: Bun
- Node.js (v18+)
- Bun
- PostgreSQL
- S3-compatible storage (for image uploads)
- Firebase project (for push notifications)
bun installCreate a .env file in the project root:
cp .env.example .envadd your credentials
Place your Firebase service account key file at src/utils/serviceAccountKey.json. You can download this from the Firebase Console under Project Settings → Service accounts → Generate new private key.
# Run migrations
bunx prisma migrate deploy --config prisma.config.ts
# Generate Prisma client
bunx prisma generate --config prisma.config.ts# Development (with auto-reload)
bun run dev
# Production
bun run startAll protected endpoints require a Bearer token in the Authorization header:
Authorization: Bearer <access_token>
The access token expires in 1 hour. Use the /api/auth/refresh endpoint to get a new one (the refresh token is stored as an HTTP-only cookie and expires in 7 days).
Register a new user account.
Request Body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "P@ssw0rd!",
"role": "user"
}| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | User's full name |
email |
string | Yes | Must be unique |
password |
string | Yes | Plain-text password (hashed on server) |
role |
string | No | "user" (default) or "admin" |
Response 201:
{
"message": "User registered successfully",
"data": {
"token": "eyJhbGciOiJIUzI1NiIs..."
}
}A refreshToken cookie is also set automatically.
Error 400:
{
"message": "User with this email already exists."
}Login with existing credentials.
Request Body:
{
"email": "john@example.com",
"password": "P@ssw0rd!"
}| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | User email |
password |
string | Yes | Password |
Response 200:
{
"message": "Login successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiIs..."
}
}A refreshToken cookie is also set automatically.
Error 401:
{
"message": "Invalid email or password"
}Refresh the access token using the refresh token cookie.
Request Body: None
Cookies Required: refreshToken (set automatically by login/register)
Response 200:
{
"message": "Token refreshed successfully",
"data": {
"token": "eyJhbGciOiJIUzI1NiIs..."
}
}Error 401:
{
"message": "Refresh token not provided"
}{
"message": "Invalid refresh token"
}Logout and clear the refresh token cookie. Requires authentication.
Headers:
Authorization: Bearer <access_token>
Request Body: None
Response 200:
{
"message": "Logged out successfully"
}Error 401:
{
"message": "Authorization header missing or invalid"
}All user endpoints require authentication.
Get the authenticated user's profile with enrollments.
Headers:
Authorization: Bearer <access_token>
Response 200:
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "USER",
"fcmToken": null,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"enrollments": [
{
"id": 1,
"userId": 1,
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"course": {
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": null,
"price": 0,
"published": false,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}
]
}Error 404:
{
"message": "User not found"
}Update the authenticated user's profile.
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"name": "Jane Doe",
"email": "jane@example.com"
}| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | User's full name |
email |
string | No | User email |
Response 200:
{
"id": 1,
"name": "Jane Doe",
"email": "jane@example.com",
"role": "USER",
"fcmToken": null,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T05:00:00.000Z"
}Delete the authenticated user's account.
Headers:
Authorization: Bearer <access_token>
Request Body: None
Response 200:
{
"message": "User profile deleted successfully"
}Store or update the user's FCM push notification token.
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"fcmToken": "eJ7xk9..."
}| Field | Type | Required | Description |
|---|---|---|---|
fcmToken |
string | Yes | Firebase Cloud Messaging token |
Response 200:
{
"message": "FCM token updated successfully"
}Error 400:
{
"message": "FCM token is required"
}All course endpoints require authentication.
Get all courses with their lessons and enrollments.
Headers:
Authorization: Bearer <access_token>
Response 200:
{
"message": "Courses fetched successfully",
"data": [
{
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": "https://example.com/js.png",
"price": 29.99,
"published": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"lessons": [
{
"id": 1,
"title": "Variables and Types",
"content": "In this lesson...",
"videoId": "https://example.com/video1.mp4",
"courseId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
],
"enrollments": [
{
"id": 1,
"userId": 1,
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "USER",
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}
],
"isEnrolled": true
}
]
}Error 500:
{
"error": "Internal server error"
}Get a single course by ID.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
id |
number | Course ID |
Response 200:
{
"message": "Course fetched successfully",
"data": {
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": "https://example.com/js.png",
"price": 29.99,
"published": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"lessons": [],
"enrollments": [],
"isEnrolled": false
}
}Error 404:
{
"error": "Course not found"
}Enroll the authenticated user in a course.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
courseId |
number | Course ID |
Request Body: None
Response 200:
{
"message": "Enrolled in course successfully",
"data": {
"id": 1,
"userId": 1,
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}Error 400:
{
"error": "User is already enrolled in this course"
}Error 500:
{
"error": "Internal server error"
}Get all enrollments for the authenticated user.
Headers:
Authorization: Bearer <access_token>
Response 200:
{
"message": "Enrollments fetched successfully",
"data": [
{
"id": 1,
"userId": 1,
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"course": {
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": "https://example.com/js.png",
"price": 29.99,
"published": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"lessons": []
}
}
]
}All lesson endpoints require authentication.
Get all lessons for a specific course.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
courseId |
number | Course ID |
Response 200:
{
"message": "Lessons fetched successfully",
"data": [
{
"id": 1,
"title": "Variables and Types",
"content": "In this lesson...",
"videoId": "https://example.com/video1.mp4",
"courseId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"course": {
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": null,
"price": 0,
"published": false,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
},
"progress": [],
"isCompleted": false
}
]
}Error 500:
{
"error": "Internal server error"
}Get a single lesson by ID.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
id |
number | Lesson ID |
Response 200:
{
"message": "Lesson fetched successfully",
"data": {
"id": 1,
"title": "Variables and Types",
"content": "In this lesson...",
"videoId": "https://example.com/video1.mp4",
"courseId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"course": {
"id": 1,
"title": "Introduction to JavaScript"
},
"progress": [],
"isCompleted": false
}
}Error 404:
{
"error": "Lesson not found"
}Mark a lesson as completed for the authenticated user. A LessonProgress record must already exist (see the progress tracking flow below).
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
lessonId |
number | Lesson ID |
Request Body: None
Response 200:
{
"message": "Lesson completed successfully",
"data": {
"id": 1,
"userId": 1,
"lessonId": 1,
"completed": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T05:00:00.000Z"
}
}Error 500:
{
"error": "Internal server error"
}Update a lesson progress record.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
lessonTrackId |
number | LessonProgress ID |
Request Body:
{
"completed": true
}| Field | Type | Required | Description |
|---|---|---|---|
completed |
boolean | No | Completion status |
Response 200:
{
"message": "Lesson track updated successfully",
"data": {
"id": 1,
"userId": 1,
"lessonId": 1,
"completed": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T05:00:00.000Z"
}
}Error 500:
{
"error": "Internal server error"
}All quiz endpoints require authentication.
Fetch the quiz for a specific course, including questions and answers.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
courseId |
number | Course ID |
Response 200:
{
"data": {
"id": 1,
"title": "JavaScript Basics Quiz",
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"questions": [
{
"id": 1,
"question": "What is JavaScript?",
"quizId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"answers": [
{
"id": 1,
"answer": "A programming language",
"correct": true,
"questionId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
]
}
]
}
}Error 404:
{
"error": "Quiz not found for this course"
}Error 500:
{
"error": "Internal server error"
}Submit a quiz attempt. Responses are graded and saved. A score of 70% or above is required to pass.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
quizId |
number | Quiz ID |
Request Body:
{
"responses": [
{
"questionId": 1,
"answerId": 1
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
responses |
array | Yes | List of quiz response objects |
responses[].questionId |
number | Yes | The question being answered |
responses[].answerId |
number | Yes | The selected answer option ID |
Response 201:
{
"message": "Quiz graded successfully",
"data": {
"attemptId": 1,
"score": 100.0,
"passed": true,
"correctCount": 1,
"totalQuestions": 1
}
}Error 404:
{
"error": "Quiz not found"
}Error 500:
{
"error": "Internal server error"
}All admin endpoints require authentication and the
adminrole.
Get all users with their enrollments.
Headers:
Authorization: Bearer <access_token>
Response 200:
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "USER",
"fcmToken": null,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"enrollments": [
{
"id": 1,
"userId": 1,
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"course": {
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": null,
"price": 0,
"published": false,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}
]
}
]Error 500:
{
"message": "Internal server error"
}Get all courses with lessons and enrollments (including enrolled user details).
Headers:
Authorization: Bearer <access_token>
Response 200:
[
{
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": "https://example.com/js.png",
"price": 29.99,
"published": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"lessons": [
{
"id": 1,
"title": "Variables and Types",
"content": "In this lesson...",
"videoId": "https://example.com/video1.mp4",
"courseId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
],
"enrollments": [
{
"id": 1,
"userId": 1,
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "USER",
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}
]
}
]Error 500:
{
"message": "Internal server error"
}Create a new course. Supports optional image upload via multipart/form-data (uploaded to S3).
Headers:
Authorization: Bearer <access_token>
Content-Type: multipart/form-data
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Course title |
description |
string | No | Course description |
price |
number | No | Course price (default 0) |
published |
boolean | No | Published status (default false) |
image |
file | No | Course image (uploaded to S3) |
Response 201:
{
"id": 2,
"title": "Advanced CSS",
"description": "Master CSS layouts",
"imageUrl": "https://bucket.example.com/1753084800000.png",
"price": 0,
"published": false,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}Response 201 (without image):
{
"id": 2,
"title": "Advanced CSS",
"description": "Master CSS layouts",
"imageUrl": null,
"price": 0,
"published": false,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}Error 500:
{
"message": "Internal server error"
}Update an existing course.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
id |
number | Course ID |
Request Body:
{
"title": "Advanced CSS — Updated",
"description": "Master modern CSS layouts",
"price": 19.99,
"published": true
}| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | Course title |
description |
string | No | Course description |
imageUrl |
string | No | Image URL |
price |
number | No | Course price |
published |
boolean | No | Published status |
Response 200:
{
"id": 2,
"title": "Advanced CSS — Updated",
"description": "Master modern CSS layouts",
"imageUrl": null,
"price": 19.99,
"published": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T05:00:00.000Z"
}Error 500:
{
"message": "Internal server error"
}Delete a course.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
id |
number | Course ID |
Request Body: None
Response 204: No content.
Error 500:
{
"message": "Internal server error"
}Get all lessons with their course and progress data.
Headers:
Authorization: Bearer <access_token>
Response 200:
[
{
"id": 1,
"title": "Variables and Types",
"content": "In this lesson...",
"videoId": "https://example.com/video1.mp4",
"courseId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"course": {
"id": 1,
"title": "Introduction to JavaScript",
"description": "Learn JS from scratch",
"imageUrl": null,
"price": 0,
"published": false,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
},
"progress": []
}
]Error 500:
{
"message": "Internal server error"
}Create a new lesson.
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"title": "Functions and Scope",
"content": "In this lesson we cover...",
"videoId": "https://example.com/video2.mp4",
"courseId": 1,
"sequence": 2
}| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Lesson title |
content |
string | Yes | Lesson content (text/HTML) |
videoId |
string | No | Video URL/ID |
courseId |
number | Yes | Parent course ID |
sequence |
number | Yes | Sequence order |
Response 201:
{
"id": 2,
"title": "Functions and Scope",
"content": "In this lesson we cover...",
"videoId": "https://example.com/video2.mp4",
"courseId": 1,
"sequence": 2,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}Error 500:
{
"message": "Internal server error"
}Update an existing lesson.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
id |
number | Lesson ID |
Request Body:
{
"title": "Functions and Scope — Updated",
"content": "Updated lesson content...",
"videoId": "https://example.com/video2-v2.mp4"
}| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | Lesson title |
content |
string | No | Lesson content |
videoId |
string | No | Video URL/ID |
courseId |
number | No | Parent course ID |
sequence |
number | No | Sequence order |
Response 200:
{
"id": 2,
"title": "Functions and Scope — Updated",
"content": "Updated lesson content...",
"videoId": "https://example.com/video2-v2.mp4",
"courseId": 1,
"sequence": 2,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T05:00:00.000Z"
}Error 500:
{
"message": "Internal server error"
}Delete a lesson.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
id |
number | Lesson ID |
Request Body: None
Response 204: No content.
Error 500:
{
"message": "Internal server error"
}Send a push notification to a specific device via Firebase Cloud Messaging.
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"title": "New Course Available!",
"body": "Check out our latest course on React.",
"token": "eJ7xk9..."
}| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Notification title |
body |
string | Yes | Notification body text |
token |
string | Yes | Target device's FCM token |
Response 200:
{
"message": "Notification sent successfully",
"response": "projects/my-project/messages/1234567890"
}Error 400:
{
"message": "Title, body, and token are required"
}{
"message": "Invalid FCM token. Token has been removed."
}Get all FCM tokens for all users.
Headers:
Authorization: Bearer <access_token>
Response 200:
{
"fcmTokens": ["eJ7xk9...", "fK8yL0...", null]
}Get the FCM token for a specific user.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
userId |
number | User ID |
Response 200:
{
"fcmToken": "eJ7xk9..."
}Error 404:
{
"message": "FCM token not found"
}Get the quiz for a specific course, including questions and answers.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
courseId |
number | Course ID |
Response 200:
{
"data": {
"id": 1,
"title": "JavaScript Basics Quiz",
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"questions": [
{
"id": 1,
"question": "What is JavaScript?",
"quizId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"answers": [
{
"id": 1,
"answer": "A programming language",
"correct": true,
"questionId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
]
}
]
}
}Error 404:
{
"error": "Quiz not found for this course"
}Error 500:
{
"error": "Internal server error"
}Create a new quiz for a course.
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"courseId": 1,
"title": "JavaScript Basics Quiz"
}| Field | Type | Required | Description |
|---|---|---|---|
courseId |
number | Yes | Parent course ID |
title |
string | Yes | Quiz title |
Response 201:
{
"data": {
"id": 1,
"title": "JavaScript Basics Quiz",
"courseId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}Error 400:
{
"error": "Invalid quiz data"
}Error 500:
{
"error": "Internal server error"
}Create a question in a quiz.
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"quizId": 1,
"question": "What is JS?",
"sequence": 1
}| Field | Type | Required | Description |
|---|---|---|---|
quizId |
number | Yes | Parent quiz ID |
question |
string | Yes | The question text |
sequence |
number | Yes | Sequence order of questions |
Response 201:
{
"data": {
"id": 1,
"question": "What is JS?",
"quizId": 1,
"sequence": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}Error 400:
{
"error": "Invalid question data"
}Error 500:
{
"error": "Internal server error"
}Create an answer option for a question.
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"questionId": 1,
"answer": "A programming language",
"correct": true
}| Field | Type | Required | Description |
|---|---|---|---|
questionId |
number | Yes | Parent question ID |
answer |
string | Yes | Answer option text |
correct |
boolean | Yes | Whether this answer option is correct |
Response 201:
{
"data": {
"id": 1,
"answer": "A programming language",
"correct": true,
"questionId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}Error 400:
{
"error": "Invalid answer data"
}Error 500:
{
"error": "Internal server error"
}Get all quiz attempts for a specific user.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
userId |
number | User ID |
Response 200:
{
"data": [
{
"id": 1,
"userId": 1,
"quizId": 1,
"score": 100,
"passed": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"answers": [
{
"id": 1,
"attemptId": 1,
"answerId": 1,
"answer": {
"id": 1,
"answer": "A programming language",
"correct": true,
"questionId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}
]
}
]
}Error 500:
{
"error": "Internal server error"
}Get details of a specific quiz attempt.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
attemptId |
number | Quiz attempt ID |
Response 200:
{
"data": {
"id": 1,
"userId": 1,
"quizId": 1,
"score": 100,
"passed": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z",
"answers": [
{
"id": 1,
"attemptId": 1,
"answerId": 1,
"answer": {
"id": 1,
"answer": "A programming language",
"correct": true,
"questionId": 1,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}
}
]
}
}Error 404:
{
"error": "Quiz attempt not found"
}Error 500:
{
"error": "Internal server error"
}Accept and approve an enrollment request. Note: The URL path is spelled /acccpet/.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
enrollmentId |
number | Enrollment ID |
Response 200:
{
"id": 1,
"userId": 1,
"courseId": 1,
"approved": true,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}Error 500:
{
"message": "Internal server error"
}Reject and disapprove an enrollment request.
Headers:
Authorization: Bearer <access_token>
Params:
| Param | Type | Description |
|---|---|---|
enrollmentId |
number | Enrollment ID |
Response 200:
{
"id": 1,
"userId": 1,
"courseId": 1,
"approved": false,
"createdAt": "2026-07-21T04:00:00.000Z",
"updatedAt": "2026-07-21T04:00:00.000Z"
}Error 500:
{
"message": "Internal server error"
}All endpoints may return the following common errors:
| Status | Response | Cause |
|---|---|---|
400 |
{ "error": "Validation error details..." } |
Request body failed validation |
401 |
{ "message": "Authorization header missing or invalid" } |
Missing or malformed Bearer token |
401 |
{ "message": "Invalid or expired token" } |
Expired or tampered access token |
401 |
{ "message": "User not found" } |
Token references a deleted user |
500 |
{ "error": "Internal server error" } |
Unexpected server-side failure |
Users ──< Enrollment >── Courses ──< Lessons
│ │ │ │
│ │ └───< Quizzes ───< Questions ───< Answers
│ │ │ ▲
│ │ └───< QuizAttempt >──< UserQuizAnswer
└──────< LessonProgress >────────────┘
- Users — name, email (unique), password, role (
USER|ADMIN), fcmToken - Courses — title, description, imageUrl, price, published
- Lessons — title, content, videoId, sequence, linked to a course
- Enrollment — links a user to a course (unique per
[userId, courseId]), contains approved status (boolean) - LessonProgress — tracks lesson completion per user (unique per
[userId, lessonId]) - Quizzes — title, linked to a course
- Questions — question, sequence, linked to a quiz
- Answers — answer, correct flag, linked to a question
- QuizAttempt — score, passed flag, linked to a user and quiz
- UserQuizAnswer — links an attempt to the selected answer option
src/
├── app.js # Express app entry point
├── features/
│ ├── admin/ # Admin routes & controller (requires admin role)
│ ├── authentication/ # Auth routes & controller
│ ├── courses/ # Course routes & controller
│ ├── lessons/ # Lesson routes & controller
│ ├── quizzes/ # Quiz routes & controller
│ └── user/ # User profile & FCM token management
├── middlewares/
│ ├── auth.middleware.js # checkAuth, isAdmin
│ └── upload.middleware.js # S3 file upload (multer-s3)
├── models/ # Prisma data-access layer
├── schemas/ # Joi validation schemas
├── utils/
│ ├── firebase.js # Firebase Admin SDK initialization
│ ├── prisma.js # Prisma client instance
│ └── s3.js # S3 client configuration
└── generated/
└── prisma/ # Generated Prisma client
| Script | Command | Description |
|---|---|---|
dev |
bun run dev |
Start with nodemon |
start |
bun run start |
Start in production |
format |
bun run format |
Format code with Prettier |
check format |
bun run format:check |
Check Format of the code with Prettier |