A production-ready backend for modern learning platforms.
A full-featured RESTful backend for an online educational platform with e-commerce, wallet, and blog capabilities.
- 🎯 About
- ✨ Features
- 🛠️ Tech Stack
- 📋 Prerequisites
- 🚀 Getting Started
- 📁 Project Structure
- 📊 API Documentation
- 🔐 Authentication
- 🌐 Environment Variables
- 📝 Scripts
- 🤝 Contributing
Course Shop API is a production-ready backend server for a full-stack educational e-commerce platform. It handles everything from user authentication and course management to payment processing, blog posts, and real-time admin notifications — all built with a clean modular architecture on top of Express 5 and TypeScript.
The platform is designed for two types of users: students who browse, purchase, and enroll in courses, and administrators who manage content, users, discounts, and monitor platform analytics.
- ✅ 🔐 Authentication & Authorization — Dual-mode JWT (Bearer token + HttpOnly cookies), email verification, forgot/reset password, change email & password flows
- ✅ 👤 User Management — Profile with avatar upload, admin ban/unban system, role-based access control (
USER/ADMIN) - ✅ 🎓 Course Management — Full CRUD for courses with image upload, slugs, levels, category & teacher associations, publish/unpublish toggle
- ✅ 🗂️ Category System — Hierarchical categories with visibility toggle, linked to both courses and blog posts
- ✅ 👨🏫 Teacher Profiles — Create and manage instructor profiles with avatar, bio, and slug
- ✅ 🛒 Shopping Cart — Persistent cart per user, add/remove courses, apply/remove discount codes
- ✅ 💳 Order & Checkout — Checkout via Zarinpal payment gateway or wallet balance, order history, cancellation
- ✅ 💰 Wallet System — Charge wallet via Zarinpal, track balance, full transaction history
- ✅ 🎟️ Discount Codes — Percentage and fixed-amount discounts, usage limits, expiry dates, admin toggle
- ✅ 📚 Enrollments — Free or paid course enrollment, access to enrolled courses
- ✅ 📝 Blog Posts — Full CRUD for posts with image upload, slug routing, publish/unpublish
- ✅ 💬 Comments — Nested comments on courses and posts, moderation (approve/reject), user's own comment management
- ✅ 👍 Reactions — Like/Dislike reactions on courses, posts, and comments
- ✅ ❤️ Favorites — Bookmark courses and blog posts
- ✅ 🔍 Global Search — Cross-entity search across courses and posts
- ✅ 📊 Admin Dashboard Stats — Overview and per-entity stats (users, courses, orders, revenue, discounts, comments, posts, enrollments)
- ✅ 🖼️ File Uploads — Cloudinary-powered image uploads for avatars, courses, posts, teachers, and rich-text editor images
- ✅ 📧 Email System — Gmail OAuth2 API (primary) with SMTP fallback via Nodemailer — for verification, password reset, and email change
- ✅ 🤖 Telegram Notifications — Real-time error alerts with stack traces, request context, user info, and device details sent to a Telegram bot
- ✅ 🛡️ Security — Helmet, CORS with allowlist, global + per-route rate limiting, input sanitization via sanitize-html, Zod schema validation
- ✅ 📖 API Docs — Full Swagger/OpenAPI 3.0 documentation at
/api-docs - ✅ 📝 Logging — Winston with console + file transports (
logs/combined.log,logs/error.log) - ✅ ⚡ Graceful Shutdown — SIGTERM/SIGINT handlers with proper DB disconnection
| Category | Technology | Purpose |
|---|---|---|
| Runtime | Node.js ≥ 20 | JavaScript runtime |
| Language | TypeScript 6 | Type-safe development |
| Framework | Express 5 | HTTP server & routing |
| Database | PostgreSQL (Neon) | Primary relational database |
| ORM | Prisma 7 | Type-safe database access & migrations |
| Authentication | JSON Web Tokens | Access & refresh token auth |
| Validation | Zod 4 | Schema validation for all inputs |
| Password Hashing | bcrypt | Secure password storage |
| File Storage | Cloudinary | Cloud image storage & transformation |
| File Upload | Multer + multer-storage-cloudinary | Multipart form handling |
| Nodemailer + Gmail OAuth2 | Transactional email delivery | |
| Payment Gateway | Zarinpal | Iranian payment processing |
| Logging | Winston | Multi-transport structured logging |
| Error Alerts | Telegram Bot API | Real-time server error notifications |
| API Documentation | Swagger UI + swagger-jsdoc | Interactive API explorer |
| Security | Helmet + CORS + express-rate-limit | HTTP security headers & rate limiting |
| HTML Sanitization | sanitize-html | XSS prevention on user content |
| Testing | Vitest + Supertest | Unit and integration tests |
| Package Manager | pnpm 10 | Fast, disk-efficient package management |
Before you begin, make sure you have the following installed:
- Node.js
>= 20.0.0 - pnpm
>= 10.0.0— install withnpm install -g pnpm - PostgreSQL
>= 14(or a Neon cloud database) - A Cloudinary account for image uploads
- A Gmail account with OAuth2 credentials (or app password for SMTP fallback)
- A Telegram Bot token (optional, for error notifications)
- A Zarinpal merchant ID (optional, for payment gateway)
# 1. Clone the repository
git clone https://github.com/your-username/course-shop-api.git
cd course-shop-api
# 2. Install dependencies
pnpm install
# 3. Set up environment variables
cp .env.example .env
# Edit .env with your actual values
# 4. Generate Prisma client
pnpm run build
# 5. Run database migrations
pnpm exec prisma migrate deploy
# 6. Start the development server
pnpm run devThe server will start on http://localhost:3000.
API documentation is available at http://localhost:3000/api-docs.
💡 Tip: Run
pnpm exec prisma studioto explore your database visually in the browser.
⚠️ Warning: Never commit your.envfile. It contains secrets like JWT keys, DB credentials, and API tokens.
course-shop-api/
│
├── src/
│ ├── app.ts # Express app setup, middleware, route mounting
│ ├── server.ts # Server entry point, graceful shutdown
│ │
│ ├── config/
│ │ ├── cloudinary.ts # Cloudinary SDK configuration
│ │ ├── multer.ts # Multer storage configs (avatar, course, post, teacher, editor)
│ │ └── swagger.ts # OpenAPI 3.0 spec assembly
│ │
│ ├── lib/
│ │ └── prisma.ts # Prisma client singleton
│ │
│ ├── middlewares/
│ │ ├── authentication.ts # JWT verification (Bearer + Cookie)
│ │ ├── authLimiter.ts # Per-route rate limiters for auth endpoints
│ │ ├── authorization.ts # Role-based access control (ADMIN/USER)
│ │ ├── errorHandler.ts # Global error handler + Telegram alerts
│ │ ├── logger.ts # Winston logger + Telegram error formatter
│ │ └── validate.ts # Zod schema validation middleware
│ │
│ ├── types/ # Global TypeScript type augmentations
│ │
│ ├── utils/
│ │ ├── AppError.ts # Custom error class
│ │ ├── asyncHandler.ts # Async route error wrapper
│ │ ├── cloudinary.ts # Cloudinary upload/delete helpers
│ │ ├── courseAccess.ts # Enrollment access check helper
│ │ ├── email.ts # Email sender (Gmail OAuth2 + SMTP fallback)
│ │ ├── getUserIdFromRequest.ts # Extract user ID from request
│ │ ├── jwt.ts # JWT sign/verify utilities
│ │ ├── pagination.ts # Pagination helper
│ │ ├── password.ts # bcrypt hash/compare helpers
│ │ ├── reactionHelper.ts # Generic reaction toggle logic
│ │ ├── sanitizeHtml.ts # HTML sanitization wrapper
│ │ ├── slug.ts # Slug generation utility
│ │ └── zarinpal.ts # Zarinpal payment request/verify
│ │
│ ├── modules/
│ │ ├── auth/ # Registration, login, email verification, password reset, email change
│ │ ├── cart/ # Shopping cart + discount application
│ │ ├── category/ # Course & post categories with visibility toggle
│ │ ├── comment/ # Nested comments on courses & posts, moderation
│ │ ├── course/ # Course CRUD, publish toggle, reactions
│ │ ├── discount/ # Discount code management (admin)
│ │ ├── enrollment/ # Course enrollment & my courses
│ │ ├── favorite/ # Course & post bookmarks
│ │ ├── health/ # Health check & ping endpoints
│ │ ├── order/ # Order checkout (wallet/zarinpal), history, cancellation
│ │ ├── overview/ # Admin analytics & statistics dashboard
│ │ ├── post/ # Blog post CRUD, publish toggle, reactions
│ │ ├── reaction/ # Like/Dislike on courses, posts, comments
│ │ ├── search/ # Global search across courses and posts
│ │ ├── teacher/ # Teacher profile CRUD
│ │ ├── upload/ # Rich-text editor image upload
│ │ ├── user/ # User profile, avatar, admin user management
│ │ └── wallet/ # Wallet balance, charge, transaction history
│ │
│ └── __tests__/ # Vitest integration tests
│
├── prisma/
│ ├── schema.prisma # Database schema (models, enums, relations)
│ └── migrations/ # Migration history (11 migrations)
│
├── generated/
│ └── prisma/ # Auto-generated Prisma client
│
├── logs/
│ ├── combined.log # All log entries
│ └── error.log # Error-level log entries
│
├── .env # Environment variables (do not commit)
├── .gitignore
├── package.json
├── pnpm-lock.yaml
├── prisma.config.ts
└── README.md
Each module follows a consistent structure:
modules/<name>/
├── <name>.controller.ts # Request handlers (thin layer, delegates to service)
├── <name>.routes.ts # Express Router with middleware chain
├── <name>.service.ts # Business logic and Prisma queries
├── <name>.swagger.ts # OpenAPI path definitions for this module
├── <name>.validator.ts # Zod schemas for request validation
└── <name>.types.ts # TypeScript interfaces/types (where applicable)
Interactive API documentation is available via Swagger UI:
- Development:
http://localhost:3000/api-docs - Production:
https://your-domain.com/api-docs - JSON:
https://your-domain.com/swagger.json
All endpoints are documented with request schemas, response examples, and authentication requirements. The Swagger UI supports persistent authorization — log in once and all protected requests will automatically include your token.
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/health |
Full health check with DB status | ❌ |
HEAD |
/health |
Lightweight availability check | ❌ |
GET |
/health/ping |
Simple ping response | ❌ |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
POST |
/api/auth/register |
Register a new user (rate-limited) | ❌ |
POST |
/api/auth/verify-email |
Verify email with code | ❌ |
POST |
/api/auth/login |
Login and receive tokens (rate-limited) | ❌ |
POST |
/api/auth/refresh |
Refresh access token using refresh token | ❌ |
POST |
/api/auth/logout |
Logout and clear tokens | ❌ |
POST |
/api/auth/forgot-password |
Request password reset code (rate-limited) | ❌ |
POST |
/api/auth/reset-password |
Reset password with code (rate-limited) | ❌ |
POST |
/api/auth/resend-verification |
Resend email verification code | ❌ |
POST |
/api/auth/resend-reset-code |
Resend password reset code | ❌ |
POST |
/api/auth/change-password |
Change password (must know current password) | ✅ User |
POST |
/api/auth/request-change-email |
Request email change with verification code | ✅ User |
POST |
/api/auth/verify-change-email |
Confirm email change with code | ✅ User |
POST |
/api/auth/resend-change-email-code |
Resend email change verification code | ✅ User |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/users/profile |
Get own profile | ✅ User |
GET |
/api/users/profile/overview |
Get own profile with stats overview | ✅ User |
PUT |
/api/users/profile |
Update profile (name, phone, avatar upload) | ✅ User |
DELETE |
/api/users/profile/avatar |
Delete own avatar | ✅ User |
GET |
/api/users |
List all users (paginated, filterable) | ✅ Admin |
GET |
/api/users/blacklist |
List all banned users | ✅ Admin |
GET |
/api/users/:id |
Get user by ID | ✅ Admin |
POST |
/api/users/:id/ban |
Ban a user | ✅ Admin |
DELETE |
/api/users/:id/ban |
Unban a user | ✅ Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/categories |
List all visible categories | ❌ |
GET |
/api/categories/:slug |
Get category by slug with courses/posts | ❌ |
GET |
/api/categories/admin |
List all categories (including hidden) | ✅ Admin |
POST |
/api/categories |
Create a new category | ✅ Admin |
PUT |
/api/categories/:id |
Update a category | ✅ Admin |
DELETE |
/api/categories/:id |
Delete a category | ✅ Admin |
PATCH |
/api/categories/:id/visibility |
Toggle category visibility | ✅ Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/teachers |
List all teachers | ❌ |
GET |
/api/teachers/:slug |
Get teacher profile by slug | ❌ |
POST |
/api/teachers |
Create teacher (with avatar upload) | ✅ Admin |
PUT |
/api/teachers/:id |
Update teacher (with avatar upload) | ✅ Admin |
DELETE |
/api/teachers/:id |
Delete teacher | ✅ Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/courses |
List published courses (filterable, paginated) | ❌ |
GET |
/api/courses/:slug |
Get course details by slug | ❌ |
GET |
/api/courses/admin |
List all courses including unpublished | ✅ Admin |
POST |
/api/courses |
Create course (with image upload) | ✅ Admin |
PUT |
/api/courses/:id |
Update course (with image upload) | ✅ Admin |
DELETE |
/api/courses/:id |
Delete course | ✅ Admin |
PATCH |
/api/courses/:id/publish |
Toggle course publish status | ✅ Admin |
POST |
/api/courses/:id/reaction |
Like or dislike a course | ✅ User |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/enrollments/my-courses |
Get user's enrolled courses | ✅ User |
POST |
/api/enrollments/:slug |
Enroll in a course by slug | ✅ User |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/cart |
Get current cart with items and totals | ✅ User |
POST |
/api/cart/items |
Add a course to cart | ✅ User |
DELETE |
/api/cart/items/:courseId |
Remove a course from cart | ✅ User |
DELETE |
/api/cart |
Clear entire cart | ✅ User |
POST |
/api/cart/apply-discount |
Apply a discount code to cart | ✅ User |
DELETE |
/api/cart/discount |
Remove applied discount from cart | ✅ User |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/orders/verify |
Verify payment after Zarinpal redirect | ❌ |
GET |
/api/orders/my-orders |
Get user's order history | ✅ User |
POST |
/api/orders/checkout/wallet |
Checkout using wallet balance | ✅ User |
POST |
/api/orders/checkout/zarinpal |
Checkout using Zarinpal payment gateway | ✅ User |
GET |
/api/orders/:id |
Get order details by ID | ✅ User |
PATCH |
/api/orders/:id/cancel |
Cancel own pending order | ✅ User |
GET |
/api/orders/admin |
List all orders (paginated, filterable) | ✅ Admin |
GET |
/api/orders/admin/:id |
Get any order details by ID | ✅ Admin |
PATCH |
/api/orders/admin/:id/cancel |
Cancel any pending order | ✅ Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/wallet |
Get wallet balance | ✅ User |
POST |
/api/wallet/charge |
Initiate wallet charge via Zarinpal | ✅ User |
GET |
/api/wallet/verify |
Verify wallet charge after redirect | ❌ |
GET |
/api/wallet/transactions |
Get own transaction history | ✅ User |
GET |
/api/wallet/admin/wallets |
List all user wallets | ✅ Admin |
GET |
/api/wallet/admin/transactions |
List all transactions | ✅ Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/discounts |
List all discount codes | ✅ Admin |
POST |
/api/discounts |
Create a discount code | ✅ Admin |
PATCH |
/api/discounts/:id/toggle |
Toggle discount active status | ✅ Admin |
DELETE |
/api/discounts/:id |
Delete a discount code | ✅ Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/posts |
List published posts (paginated) | ❌ |
GET |
/api/posts/:slug |
Get post by slug | ❌ |
GET |
/api/posts/admin |
List all posts including drafts | ✅ Admin |
POST |
/api/posts |
Create post (with image upload) | ✅ Admin |
PUT |
/api/posts/:id |
Update post (with image upload) | ✅ Admin |
DELETE |
/api/posts/:id |
Delete post | ✅ Admin |
PATCH |
/api/posts/:id/publish |
Toggle post publish status | ✅ Admin |
POST |
/api/posts/:id/reaction |
Like or dislike a post | ✅ User |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/comments/course/:slug |
Get approved comments for a course | ❌ |
GET |
/api/comments/post/:slug |
Get approved comments for a post | ❌ |
POST |
/api/comments |
Create a comment (or reply) | ✅ User |
GET |
/api/comments/my-comments |
Get own comments | ✅ User |
DELETE |
/api/comments/:id |
Delete own comment | ✅ User |
POST |
/api/comments/:id/reaction |
Like or dislike a comment | ✅ User |
GET |
/api/comments/admin |
List all comments for moderation | ✅ Admin |
PATCH |
/api/comments/:id/approve |
Approve a pending comment | ✅ Admin |
PATCH |
/api/comments/:id/reject |
Reject a pending comment | ✅ Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
POST |
/api/favorites/courses/:courseId |
Toggle course bookmark | ✅ User |
GET |
/api/favorites/courses |
Get own bookmarked courses | ✅ User |
POST |
/api/favorites/posts/:postId |
Toggle post bookmark | ✅ User |
GET |
/api/favorites/posts |
Get own bookmarked posts | ✅ User |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/search |
Search across courses and posts | ❌ |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
POST |
/api/uploads/editor-image |
Upload image for rich-text editor | ✅ Admin |
All overview endpoints are admin-only.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/overview/admin |
Full dashboard summary |
GET |
/api/overview/admin/users |
User registration stats |
GET |
/api/overview/admin/courses |
Course stats |
GET |
/api/overview/admin/orders |
Order & sales stats |
GET |
/api/overview/admin/revenue |
Revenue breakdown |
GET |
/api/overview/admin/discounts |
Discount usage stats |
GET |
/api/overview/admin/comments |
Comment moderation stats |
GET |
/api/overview/admin/posts |
Post engagement stats |
GET |
/api/overview/admin/enrollments |
Enrollment stats |
This API uses a dual-authentication mechanism designed for both web and mobile clients.
Upon successful login or email verification, the server automatically sets two secure, HttpOnly cookies:
accessToken— short-lived JWT for API accessrefreshToken— long-lived JWT used to obtain a new access token
Cookies are sent automatically by the browser on every request — no extra code needed.
The accessToken is also returned in the JSON response body. Include it in the Authorization header:
Authorization: Bearer <your_access_token>When the access token expires, call POST /api/auth/refresh. The server reads the refreshToken cookie (or accepts it from the request body) and issues a new access token.
| Role | Permissions |
|---|---|
USER |
Access own profile, enroll in courses, manage cart/orders, post comments, reactions, favorites |
ADMIN |
All USER permissions + full CRUD on courses, categories, teachers, posts, discounts, user management, stats |
📝 Note: Protected routes first check for an
accessTokencookie, then fall back to theAuthorization: Bearerheader.
Copy .env.example to .env and fill in all required values.
| Variable | Description | Required |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string (supports Neon pooler) | ✅ |
PORT |
Server port (default: 3000) |
✅ |
JWT_ACCESS_SECRET |
Secret key for signing access tokens | ✅ |
JWT_REFRESH_SECRET |
Secret key for signing refresh tokens | ✅ |
FRONTEND_URL |
Frontend app URL (for CORS & email links) | ✅ |
BACKEND_URL |
Backend API base URL | ✅ |
CORS_ORIGINS |
Comma-separated list of allowed CORS origins | ✅ |
EMAIL_USER |
Gmail address for SMTP fallback | ✅ |
EMAIL_PASS |
Gmail app password for SMTP fallback | ✅ |
GMAIL_CLIENT_ID |
Google OAuth2 client ID | |
GMAIL_CLIENT_SECRET |
Google OAuth2 client secret | |
GMAIL_REFRESH_TOKEN |
Google OAuth2 refresh token | |
GMAIL_SENDER_EMAIL |
Sender email address for Gmail API | |
CLOUDINARY_CLOUD_NAME |
Cloudinary cloud name | ✅ |
CLOUDINARY_API_KEY |
Cloudinary API key | ✅ |
CLOUDINARY_API_SECRET |
Cloudinary API secret | ✅ |
CLOUDINARY_BASE_FOLDER |
Root folder in Cloudinary for uploaded assets | ✅ |
ZARINPAL_MERCHANT_ID |
Zarinpal merchant ID (use sandbox UUID for testing) | ✅ |
TELEGRAM_BOT_TOKEN |
Telegram bot token for error notifications | |
TELEGRAM_CHAT_ID |
Telegram chat/channel ID to receive alerts | |
ENABLE_TEST_ROUTES |
Set to true to enable test error routes in dev |
⚠️ Warning: IfGMAIL_CLIENT_IDandGMAIL_REFRESH_TOKENare set, the Gmail OAuth2 API is used for sending emails. Otherwise, the system falls back to SMTP withEMAIL_USERandEMAIL_PASS.
| Script | Command | Description |
|---|---|---|
| Development | pnpm run dev |
Start server with nodemon (hot reload) |
| Production | pnpm run start |
Generate Prisma client and start server |
| Build | pnpm run build |
Generate Prisma client |
| Test | pnpm run test |
Run all tests once with Vitest |
| Test Watch | pnpm run test:watch |
Run tests in watch mode |
| Test UI | pnpm run test:ui |
Open Vitest browser UI |
| DB Migrate | pnpm exec prisma migrate dev |
Create and apply a new migration (dev) |
| DB Deploy | pnpm exec prisma migrate deploy |
Apply pending migrations (production) |
| DB Studio | pnpm exec prisma studio |
Open Prisma Studio visual DB browser |
| DB Generate | pnpm exec prisma generate |
Regenerate Prisma client after schema changes |
The database is powered by PostgreSQL with Prisma ORM. Here's a summary of the core models:
Click to expand schema overview
| Model | Description |
|---|---|
User |
Platform users with roles (USER/ADMIN), verification, ban status, and reset/change-email flows |
Wallet |
One-to-one with User, stores balance in IRR |
Transaction |
Financial record for charges, purchases, and refunds with Zarinpal authority tracking |
Category |
Taxonomy for courses and posts, with visibility toggle |
Teacher |
Instructor profiles with bio, avatar, and slug |
Course |
Courses with pricing, level (BEGINNER/INTERMEDIATE/ADVANCED), slug, and publish status |
Enrollment |
Many-to-many between User and Course, records price paid at time of purchase |
Cart |
One per user, holds CartItems and an optional discount code |
CartItem |
Junction between Cart and Course |
Order |
Purchase record with subtotal, discount, total, and payment method |
OrderItem |
Snapshot of course data at time of purchase (preserves history) |
Post |
Blog posts with rich content, image, slug, and publish status |
Comment |
Nested comments on Courses or Posts with moderation status (PENDING/APPROVED/REJECTED) |
Reaction |
Like/Dislike on Courses, Posts, or Comments (one per user per target) |
CourseFavorite |
User bookmarks for courses |
BlogFavorite |
User bookmarks for blog posts |
Discount |
Discount codes with type (PERCENTAGE/AMOUNT), usage limits, and expiry |
Contributions are welcome. Please follow this workflow:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature-name - Commit your changes:
git commit -m 'feat: add your feature' - Push to the branch:
git push origin feature/your-feature-name - Open a Pull Request against
main
Please ensure your code follows the existing style and all tests pass before submitting a PR.
# Run tests before pushing
pnpm run testMade with ❤️ by Abbas Rostami
⭐ Star this repo if you find it useful!