A Node.js and Express backend for tracking user subscriptions. It leverages MongoDB via Mongoose for data persistence, employs JSON Web Tokens for authentication, and integrates Arcjet for bot detection and rate limiting. The application is modularized into configuration, models, controllers, routes, and middleware layers for maintainability and scalability.
Loads environment variables from a file corresponding to the current NODE_ENV (defaulting to development).
- dotenv reads
.env.development.localor.env.production.local. - Exports critical values:
PORT,NODE_ENV,DB_URI,JWT_SECRET,JWT_EXPIRES_IN,ARCJET_ENV,ARCJET_KEY.
Configures Arcjet to protect against common attacks:
- shield: SQL injection protection
- detectBot: Bot detection with live blocking
- tokenBucket: Rate limiting (5 token refill every 10 s, capacity 10)
All rules run in LIVE mode, key sourced from ARCJET_KEY.
Establishes a Mongoose connection using DB_URI.
- Throws an error without
DB_URI. - Logs success or exits on failure.
import mongoose from 'mongoose';
import { DB_URI, NODE_ENV } from '../config/env.js';
if (!DB_URI) {
throw new Error('Please define the MONGODB_URI environment variable.');
}
const connectToDatabase = async () => {
try {
await mongoose.connect(DB_URI);
console.log(`Connected to database in ${NODE_ENV} mode`);
} catch (error) {
console.error('Database connection error:', error);
process.exit(1);
}
};
export default connectToDatabase;Defines users with validation rules:
| Field | Type | Constraints |
|---|---|---|
name |
String | Required, 2–50 chars, trimmed |
email |
String | Required, unique, trimmed, lowercase, valid |
password |
String | Required, min 6 chars |
Timestamps are added automatically.
const userSchema = new mongoose.Schema({
name: { type: String, required: true, minLength: 2, maxLength: 50, trim: true },
email: {
type: String, required: true, unique: true,
lowercase: true, match: [/\S+@\S+\.\S+/, 'Invalid email']
},
password: { type: String, required: true, minLength: 6 }
}, { timestamps: true });
export default mongoose.model('User', userSchema);Captures subscription details and auto-computes renewalDate.
| Field | Type | Constraints |
|---|---|---|
name |
String | Required, 2–100 chars |
price |
Number | Required, 0–1000 |
currency |
String | Enum: USD, ZAR, GBP (default ZAR) |
frequency |
String | Enum: daily, weekly, monthly, yearly |
category |
String | Enum: sport, news, entertainment, lifestyle, technology |
paymentMethod |
String | Required |
status |
String | Enum: active, cancelled, expired (default active) |
startDate |
Date | Required, must be ≤ current date |
renewalDate |
Date | Required, must be > startDate; calculated in a pre-save hook if omitted |
user |
ObjectId | Reference to User, required, indexed |
subscriptionSchema.pre('save', function (next) {
if (!this.renewalDate) {
const periods = { daily: 1, weekly: 7, monthly: 30, yearly: 365 };
this.renewalDate = new Date(this.startDate);
this.renewalDate.setDate(this.renewalDate.getDate() + periods[this.frequency]);
}
if (this.renewalDate < new Date()) {
this.status = 'expired';
}
next();
});
export default mongoose.model('Subscription', subscriptionSchema);express.json()express.urlencoded({ extended: false })
Parses cookies into req.cookies.
Applies Arcjet protection to all requests. Blocks or logs based on decision.
Verifies JWT from Authorization: Bearer <token>.
- Attaches
req.useron success. - Returns 401 on failure.
Catches errors and responds with:
- 404 on Mongoose
CastError - 400 on duplicate key or validation errors
- 500 for others
Logs original error to console.
Handles sign-up, sign-in, and sign-out operations.
// Sign Up
export const signUp = async (req, res, next) => {
// Creates user, hashes password, issues JWT
};
// Sign In
export const signIn = async (req, res, next) => {
// Verifies credentials, issues JWT
};
// Sign Out
export const signOut = async (req, res, next) => {
// TODO: Implement token invalidation or cookie clearing
};Provides user retrieval endpoints.
export const getUsers = async (req, res, next) => { /* Lists all users */ };
export const getUser = async (req, res, next) => { /* Gets one user */ };Handles subscription creation.
export const createSubscription = async (req, res, next) => {
// Associates subscription with req.user and saves
};- POST
/api/v1/auth/sign-up→signUp - POST
/api/v1/auth/sign-in→signIn - POST
/api/v1/auth/sign-out→signOut
- GET
/api/v1/users/→getUsers - GET
/api/v1/users/:id→getUser(protected) - POST
/api/v1/users/→ Placeholder - PUT
/api/v1/users/:id→ Placeholder - DELETE
/api/v1/users/:id→ Placeholder
- GET
/api/v1/subscriptions/→ Placeholder - GET
/api/v1/subscriptions/:id→ Placeholder - POST
/api/v1/subscriptions/→createSubscription(protected) - PUT
/api/v1/subscriptions/:id→ Placeholder - DELETE
/api/v1/subscriptions/:id→ Placeholder - PUT
/api/v1/subscriptions/user/id→ Placeholder - PUT
/api/v1/subscriptions/:id/cancel→ Placeholder - GET
/api/v1/subscriptions/upcoming-renewals→ Placeholder
Bootstraps the Express server:
- Applies parsers, cookie and Arcjet middleware
- Mounts routers under
/api/v1/… - Global error handler
- Serves a welcome message at
/ - Listens on
PORTand connects to MongoDB on startup
import express from 'express';
import { PORT } from './config/env.js';
import connectToDatabase from './database/mongodb.js';
import arcjeMiddleware from './middlewares/arcjet.middleware.js';
// … other imports …
const app = express();
app.use(express.json(), express.urlencoded({ extended: false }), cookieParser(), arcjeMiddleware);
// Mount routers and error handler…
app.listen(PORT, async () => {
console.log(`Running on http://localhost:${PORT}`);
await connectToDatabase();
});
export default app;| Script | Command |
|---|---|
start |
node app.js |
dev |
nodemon app.js |
Key dependencies include express, mongoose, jsonwebtoken, bcryptjs, cookie-parser, and @arcjet/node for security.
❤️ Happy coding!