├── .DS_Store ├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── classes ├── htmlProcessor.js ├── mailer.js └── userHandler.js ├── db ├── db.js ├── mysql.js └── postgres.js ├── logger.js ├── middlewares ├── authenticate.js └── disabled.js ├── package.json ├── routes ├── index.js ├── log.js ├── users.js └── validation.js └── runtime └── runtime.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharko99/light-api/a0791afd09dc9ba19dc7ad2b4c8bc9ef3b26e591/.DS_Store -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Application settings 2 | PORT=5005 3 | JWT_SECRET=your_jwt_secret 4 | 5 | # Light API settings 6 | HIDE_USERID=true # Hide user id in the user token 7 | 8 | # Database Type 9 | DBTYPE=mysql 10 | 11 | # MySQL2 settings 12 | DB_HOST=localhost 13 | DB_USER=root 14 | DB_PASSWORD=password 15 | DB_NAME=testdb 16 | 17 | # PostGres settings 18 | PGHOST=localhost 19 | PGUSER=root 20 | PGPASSWORD=PASSWORD 21 | PGNAME=testdb 22 | 23 | # NodeMailer settings 24 | EMAIL_HOST=smtp.example.com 25 | EMAIL_PORT=587 26 | EMAIL_SECURE=false 27 | EMAIL_USER=your_email@example.com 28 | EMAIL_PASS=your_email_password 29 | EMAIL_FROM='Your Name ' -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | combined.log 3 | error.log 4 | .env 5 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Logan B. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌟 LightAPI - Lightweight Express Boilerplate 2 | 3 | Welcome to **LightAPI**, the most modular, easy-to-use, and feature-rich Express API template! Whether you're a beginner or an experienced developer, LightAPI provides a solid foundation to kickstart your project. 4 | 5 | ## 📝 Change Log 6 | Date format: YYYY-MM-DD 7 | 8 | ### 🚀 1.0.3 - 2024-06-05 9 | - 🐰 Added Bun compatibility ([kush-js](https://github.com/kush-js)) 10 | 11 | ### 🚀 1.0.2 - 2024-06-04 12 | - 🔧 Added a middleware to disable routes easily: `app.get('/register', disabled, (req, res) => {` => This route now returns a 403 error 13 | 14 | ### 🚀 1.0.1 - 2024-06-04 15 | - 🐘 Added postgreSQL support. Credits: [kush-js](https://github.com/kush-js) 16 | - 🔄 Changed the default port to 5005 17 | - 🔒 Added a field in the .env to hide user id from userHandler token 18 | - 📜 Added Authorization header in the routes comments examples 19 | - ✅ Protected routes and MySQL2 functions have been tested and are working 20 | - 🧪 Still need a complete test for postgreSQL (Your PR are welcome :)) 21 | 22 | ### 🚀 1.0.0 - 2024-06-03 23 | - 🎉 Initial release 24 | 25 | 26 | ## Features 27 | 28 | LightAPI comes packed with a variety of powerful features: 29 | 30 | - 🔄 **Routes handling**: Easily define and manage your API routes. 31 | - 🔐 **User authentication with JWT**: Secure user authentication out of the box. 32 | - 💾 **MySQL2 / Postgres basic functions**: Simple and efficient MySQL2 and Postgres integration. 33 | - 🐘 **PostgreSQL support**: Switch between MySQL2 and Postgres with ease. 34 | - 📧 **Nodemailer included**: Send emails effortlessly with Nodemailer. 35 | - 🔧 **Configuration with DotEnv**: Manage environment variables with ease. 36 | - 📝 **Winston logging**: Robust logging for better debugging and monitoring. 37 | - 📡 **CORS enabled**: Cross-Origin Resource Sharing for flexible API usage. 38 | - 🚫 **Rate limiting**: Protect your API from abuse with built-in rate limiting. 39 | - 🔍 **Joi validation**: Validate incoming requests with Joi. 40 | - 🛡️ **Middleware ready**: Pre-configured middleware for common tasks. 41 | - 📦 **Modular structure**: Highly modular design for easy customization and extension. 42 | - 🔒 **Easy route disabling**: Disable routes easily with middleware. 43 | - 🐰 **Bun compatibility**: Works with Bun out of the box. Autodetects the runtime runner. 44 | - 🚀 **Works out of the box!**: Get up and running quickly with minimal configuration. 45 | 46 | ## Getting Started 47 | 48 | ### Prerequisites 49 | 50 | Make sure you have [Node.js](https://nodejs.org/) installed on your machine. 51 | 52 | ### Installation 53 | 54 | 1. Clone the repository: 55 | ```sh 56 | git clone https://github.com/yourusername/lightapi.git 57 | cd lightapi 58 | ``` 59 | 60 | 2. Install dependencies: 61 | ```sh 62 | npm install 63 | ``` 64 | Or with Bun 65 | ```sh 66 | bun install 67 | ``` 68 | 69 | 3. Copy the `.env.example` file to `.env` in the root directory and configure your environment variables: 70 | ```bash 71 | # Application settings 72 | PORT=5005 73 | JWT_SECRET=your_jwt_secret 74 | 75 | # Light API settings 76 | HIDE_USERID=true # Hide user id in the user token 77 | 78 | # Database Type 79 | DBTYPE=mysql 80 | 81 | # MySQL2 settings 82 | DB_HOST=localhost 83 | DB_USER=root 84 | DB_PASSWORD=password 85 | DB_NAME=testdb 86 | 87 | # PostGres settings 88 | PGHOST=localhost 89 | PGUSER=root 90 | PGPASSWORD=PASSWORD 91 | PGNAME=testdb 92 | 93 | # NodeMailer settings 94 | EMAIL_HOST=smtp.example.com 95 | EMAIL_PORT=587 96 | EMAIL_SECURE=false 97 | EMAIL_USER=your_email@example.com 98 | EMAIL_PASS=your_email_password 99 | EMAIL_FROM='Your Name ' 100 | ``` 101 | 102 | #### Database Tables Setup 103 | 104 | For your comfort, here are two queries that you can use on your database to create a LightAPI compatible users table: 105 | 106 | **Mysql:** 107 | 108 | ```sql 109 | CREATE TABLE users ( 110 | id INT AUTO_INCREMENT PRIMARY KEY, 111 | username VARCHAR(255) NOT NULL UNIQUE, 112 | password VARCHAR(255) NOT NULL, 113 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 114 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 115 | ); 116 | ``` 117 | 118 | **Postgres:** 119 | 120 | ```sql 121 | CREATE TABLE users ( 122 | id SERIAL PRIMARY KEY, 123 | username VARCHAR(255) NOT NULL UNIQUE, 124 | password VARCHAR(255) NOT NULL, 125 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, 126 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP 127 | ); 128 | 129 | -- Trigger and function to update the updated_at field automatically 130 | CREATE OR REPLACE FUNCTION update_updated_at_column() 131 | RETURNS TRIGGER AS $$ 132 | BEGIN 133 | NEW.updated_at = NOW(); 134 | RETURN NEW; 135 | END; 136 | $$ LANGUAGE plpgsql; 137 | 138 | CREATE TRIGGER update_users_updated_at 139 | BEFORE UPDATE ON users 140 | FOR EACH ROW 141 | EXECUTE FUNCTION update_updated_at_column(); 142 | ``` 143 | 144 | ### Usage 145 | 146 | 1. Start the server: 147 | ```sh 148 | node app.js 149 | ``` 150 | with Bun: 151 | ```sh 152 | bun run app.js 153 | ``` 154 | 155 | Your API will be running on http://localhost:5005. 156 | 157 | ## Project Structure 158 | 159 | ```bash 160 | lightapi/ 161 | ├── app.js 162 | ├── logger.js 163 | ├── classes/ 164 | │ ├── htmlProcessor.js 165 | │ ├── mailer.js 166 | │ └── userHandler.js 167 | ├── db/ 168 | │ ├── postgres.js 169 | │ ├── mysql.js 170 | │ └── db.js 171 | ├── routes/ 172 | │ ├── index.js 173 | │ ├── log.js 174 | │ ├── user.js 175 | │ └── validation.js 176 | ├── middlewares/ 177 | │ ├── disabled.js 178 | │ └── authenticate.js 179 | ├── runtime/ 180 | │ └── runtime.js 181 | ├── node_modules/ 182 | ├── package.json 183 | ├── runtime/ 184 | └── .gitignore 185 | ``` 186 | 187 | ## Key Modules 188 | 189 | • app.js: Entry point of the application. Sets up middleware and routes. \ 190 | • db.js: Database connection and basic functions using MySQL2 or Postgres. \ 191 | • logger.js: Configured Winston logger for application-wide logging. \ 192 | • mailer.js: Nodemailer setup for sending emails. \ 193 | • userHandler.js: User-related operations, including registration and login. \ 194 | • authenticate.js: JWT authentication middleware. \ 195 | • htmlProcessor.js: Functions to process HTML files and strings with placeholders. \ 196 | • disabled.js: Middleware to disable routes. \ 197 | • routes/: Directory containing route definitions. 198 | 199 | ## Contributing 200 | We welcome contributions from the community! Please fork the repository and submit a pull request. 201 | 202 | - [kush-js](https://github.com/kush-js): Database engine switcher (interoperability) & Postgres integration 203 | - [u/MinuteScientist7254](https://www.reddit.com/user/MinuteScientist7254/): Asked the feature to hide userid from token 204 | - [kush-js](https://github.com/kush-js): Added Bun compatibility 205 | 206 | ## License 207 | This project is licensed under the MIT License. 208 | 209 | ## Contact 210 | If you have any questions, feel free to open an issue or contact us at logan+lightapi@creativehorizons.net 211 | 212 | ## My portfolio 213 | 214 | [Logan Bunelle](https://loganbunelle.com/en) 215 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const rateLimit = require('express-rate-limit'); 3 | const cors = require('cors'); 4 | const logger = require('./logger'); 5 | 6 | // Routes import 7 | const userRoutes = require('./routes/users'); 8 | const logRoutes = require('./routes/log'); 9 | const validationRoutes = require('./routes/validation'); 10 | const routes = require('./routes'); 11 | 12 | // Middlewares import 13 | const authenticate = require('./middlewares/authenticate'); 14 | const disabled = require('./middlewares/disabled'); 15 | 16 | // Application 17 | const app = express(); 18 | 19 | // Rate limiting middleware 20 | const limiter = rateLimit({ 21 | windowMs: 15 * 60 * 1000, // 15 minutes 22 | max: 100, // limit each IP to 100 requests per windowMs 23 | message: 'Too many requests from this IP, please try again after 15 minutes' 24 | }); 25 | 26 | // Apply rate limiting to all requests 27 | app.use(limiter); 28 | 29 | // Enable CORS 30 | app.use(cors()); 31 | 32 | // Middleware to parse JSON bodies 33 | app.use(express.json()); 34 | 35 | // Root url 36 | app.get('/', (req, res) => { 37 | res.status(200).json({ 38 | status: 'operational', 39 | message: 'Light API - Lightweight Express Boilerplate', 40 | features: [ 41 | '🔄 Routes handling', 42 | '🔐 User authentication with JWT', 43 | '💾 MySQL2 / Postgres basic functions', 44 | '📧 Nodemailer included', 45 | '🔧 Configuration with DotEnv', 46 | '📝 Winston logging', 47 | '📡 CORS enabled', 48 | '🚫 Rate limiting', 49 | '🔍 Joi validation', 50 | '🛡️ Middleware ready', 51 | '📦 Modular structure', 52 | '🔒 Disabled route middleware', 53 | '🚀 Works out of the box!' 54 | ] 55 | }); 56 | }); 57 | 58 | // Those routes are only examples routes to inspire you or to get you started faster. 59 | // You are not forced to use them, and can erase all routes in order to make your own. 60 | // Nested routes (routes are stored in the routes folder) 61 | app.use('/users', userRoutes); 62 | app.use('/api', authenticate, routes); // '/api' routes are protected with the 'authenticate' middleware 63 | app.use('/log', logRoutes); 64 | app.use('/validation', validationRoutes); 65 | 66 | // Root routes 67 | // curl -X GET http://localhost:5005/welcome 68 | app.get('/welcome', (req, res) => { 69 | res.json({ message: 'Welcome' }); 70 | }); 71 | 72 | // curl -X GET http://localhost:5005/disabled 73 | // This route is disabled by the middleware 74 | app.get('/disabled', disabled, (req, res) => { 75 | res.json({ message: 'This route is disabled, you cannot see this message.' }); 76 | }); 77 | 78 | const PORT = process.env.PORT || 5005; 79 | app.listen(PORT, () => { 80 | console.log(`Server is running on port ${PORT}`); 81 | // logger.info(`Server is running on port ${PORT}`); Use this line if you want to log the startup 82 | }); 83 | -------------------------------------------------------------------------------- /classes/htmlProcessor.js: -------------------------------------------------------------------------------- 1 | const runtime = require('../runtime/runtime'); 2 | 3 | /** 4 | * Replace placeholders in an HTML string with provided values. 5 | * @param {string} html - The HTML string. 6 | * @param {Object} fields - An object where keys are placeholders and values are the replacements. 7 | * @returns {string} - The processed HTML string. 8 | * @example 9 | * const processedHtml = replaceHtmlString('

{username}

', { username: 'John' }); 10 | */ 11 | function replaceHtmlString(html, fields) { 12 | let processedHtml = html; 13 | for (const [key, value] of Object.entries(fields)) { 14 | const regex = new RegExp(`{${key}}`, 'g'); 15 | processedHtml = processedHtml.replace(regex, value); 16 | } 17 | return processedHtml; 18 | } 19 | 20 | /** 21 | * Replace placeholders in an HTML file with provided values. 22 | * @param {string} filePath - The path to the HTML file. 23 | * @param {Object} fields - An object where keys are placeholders and values are the replacements. 24 | * @returns {Promise} - A promise that resolves to the processed HTML string. 25 | * @example 26 | * const processedHtml = await replaceHtmlFile('template.html', { username: 'John' }); 27 | */ 28 | async function replaceHtmlFile(filePath, fields) { 29 | let html = await runtime.readFileString(filePath); 30 | return replaceHtmlString(html, fields); 31 | } 32 | 33 | module.exports = { replaceHtmlString, replaceHtmlFile }; -------------------------------------------------------------------------------- /classes/mailer.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | require('dotenv').config(); 3 | 4 | /** 5 | * Mailer class to handle sending emails. 6 | */ 7 | class Mailer { 8 | constructor() { 9 | this.transporter = nodemailer.createTransport({ 10 | host: process.env.EMAIL_HOST, 11 | port: process.env.EMAIL_PORT, 12 | secure: process.env.EMAIL_SECURE === 'true', // true for 465, false for other ports 13 | auth: { 14 | user: process.env.EMAIL_USER, 15 | pass: process.env.EMAIL_PASS, 16 | }, 17 | }); 18 | } 19 | 20 | /** 21 | * Send an email. 22 | * @param {string} to - The recipient's email address. 23 | * @param {string} subject - The subject of the email. 24 | * @param {string} text - The plaintext content of the email. 25 | * @param {string} html - The HTML content of the email. 26 | * @returns {Promise} - A promise that resolves to the result of the email sending. 27 | * @example 28 | * const result = await mailer.sendMail('recipient@example.com', 'Subject', 'Plain text body', '

HTML body

'); 29 | */ 30 | async sendMail(to, subject, text, html) { 31 | const mailOptions = { 32 | from: process.env.EMAIL_FROM, 33 | to, 34 | subject, 35 | text, 36 | html, 37 | }; 38 | 39 | const result = await this.transporter.sendMail(mailOptions); 40 | return result; 41 | } 42 | } 43 | 44 | module.exports = new Mailer(); -------------------------------------------------------------------------------- /classes/userHandler.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const sql = require('../db/db'); 3 | const runtime = require('../runtime/runtime'); 4 | require('dotenv').config(); 5 | 6 | /** 7 | * UserHandler class to handle user registration and login. 8 | */ 9 | class UserHandler { 10 | constructor() { 11 | this.jwtSecret = process.env.JWT_SECRET || 'your_jwt_secret'; 12 | this.saltRounds = 10; 13 | } 14 | 15 | /** 16 | * Register a new user with a username and password. 17 | * @param {string} username - The username of the user. It must have the 'unique' constraint in the database to avoid duplicates. 18 | * @param {string} password - The password of the user. 19 | * @returns {Promise} - A promise that resolves to an object containing a JWT token. 20 | * @throws {Error} - If the registration fails. 21 | * @example 22 | * const token = await userHandler.registerUser('testuser', 'testpassword'); 23 | */ 24 | async registerUser(username, password) { 25 | const hashedPassword = await runtime.hash(password, this.saltRounds); 26 | const result = await sql.functions.insertRow('users', { username: username, password: hashedPassword}); 27 | if (result.affectedRows === 0) throw new Error('Failed to register user'); 28 | 29 | const token = jwt.sign({ id: process.env.HIDE_USERID ? null : result.insertId, username }, this.jwtSecret, { expiresIn: '7d' }); 30 | return token; 31 | } 32 | 33 | /** 34 | * Log in a user with a username and password. 35 | * @param {string} username - The username of the user. 36 | * @param {string} password - The password of the user. 37 | * @returns {Promise} - A promise that resolves to an object containing a JWT token. 38 | * @throws {Error} - If the login fails. 39 | * @example 40 | * const token = await userHandler.loginUser('testuser', 'testpassword'); 41 | */ 42 | async loginUser(username, password) { 43 | const user = await sql.functions.getRow('users', { username: username }); 44 | if (user.length === 0) throw new Error('User not found'); 45 | 46 | const isMatch = await runtime.compareHash(password, user.password); 47 | if (!isMatch) throw new Error('Invalid credentials'); 48 | 49 | const token = jwt.sign({ id: process.env.HIDE_USERID ? null : user.id, username: user.username }, this.jwtSecret, { expiresIn: '7d' }); 50 | return token; 51 | } 52 | 53 | /** 54 | * Verify a JWT token. Does not interact with the database to make the verification faster. 55 | * @param {string} token - The JWT token to verify. 56 | * @returns {Object} - The decoded token payload. 57 | * @throws {Error} - If the token is invalid. 58 | * @example 59 | * const decoded = userHandler.verifyToken('your_jwt_token'); 60 | */ 61 | verifyToken(token) { 62 | try { 63 | const decoded = jwt.verify(token, this.jwtSecret); 64 | return decoded; 65 | } catch (err) { 66 | throw new Error('Invalid token'); 67 | } 68 | } 69 | } 70 | 71 | module.exports = new UserHandler(); 72 | -------------------------------------------------------------------------------- /db/db.js: -------------------------------------------------------------------------------- 1 | //reads env vars and decides which database to use 2 | 3 | const sql = process.env.DBTYPE === 'postgres' ? require('./postgres') : require('./mysql'); 4 | 5 | module.exports = sql; -------------------------------------------------------------------------------- /db/mysql.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql2/promise'); 2 | require('dotenv').config(); 3 | 4 | const sql = mysql.createPool({ 5 | host: process.env.DB_HOST, 6 | user: process.env.DB_USER, 7 | password: process.env.DB_PASSWORD, 8 | database: process.env.DB_NAME, 9 | }); 10 | 11 | const functions = { 12 | /** 13 | * Get all rows from a specified table. 14 | * @param {string} table - The name of the table to query. 15 | * @returns {Promise} - A promise that resolves to an array of rows. 16 | * @example 17 | * const users = await sql.functions.getRows('users'); 18 | */ 19 | async getRows(table) { 20 | const [rows] = await sql.query(`SELECT * FROM ${table}`); 21 | return rows; 22 | }, 23 | 24 | /** 25 | * Get a specific row from a specified table using a selector. 26 | * @param {string} table - The name of the table to query. 27 | * @param {Object} selector - An object representing the selection criteria. 28 | * @returns {Promise} - A promise that resolves to a single row. 29 | * @example 30 | * const user = await sql.functions.getRow('users', { id: 1 }); 31 | */ 32 | async getRow(table, selector) { 33 | const [rows] = await sql.query(`SELECT * FROM ${table} WHERE ?`, selector); 34 | return rows[0]; 35 | }, 36 | 37 | /** 38 | * Update a row in a specified table using a selector. 39 | * @param {string} table - The name of the table to update. 40 | * @param {Object} data - An object representing the data to update. 41 | * @param {Object} selector - An object representing the selection criteria. 42 | * @returns {Promise} - A promise that resolves to the result of the update operation. 43 | * @example 44 | * const result = await sql.functions.updateRow('users', { name: 'John Doe' }, { id: 1 }); 45 | */ 46 | async updateRow(table, data, selector) { 47 | const [result] = await sql.query(`UPDATE ${table} SET ? WHERE ?`, [data, selector]); 48 | return result; 49 | }, 50 | 51 | /** 52 | * Insert a row in a specified table 53 | * @param {string} table 54 | * @param {Object} data 55 | * @returns {Promise} 56 | * @example 57 | * const result = await sql.functions.insertRow('users', { name: 'John Doe', id: 1 }); 58 | */ 59 | 60 | async insertRow(table, data) { 61 | const keys = Object.keys(data); 62 | const placeholders = keys.map(() => '?').join(', '); 63 | const values = keys.map(key => data[key]); 64 | try { 65 | const [result] = await sql.query(`INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`, values); 66 | return result; 67 | } catch (error) { 68 | console.error('Error executing query:', error); 69 | return { affectedRows: 0 }; 70 | } 71 | } 72 | } 73 | 74 | // monkey patch the pool with the functions object 75 | // not the best practice but it works for now 76 | sql.functions = functions; 77 | 78 | module.exports = sql; 79 | -------------------------------------------------------------------------------- /db/postgres.js: -------------------------------------------------------------------------------- 1 | const postgres = require('postgres'); 2 | require('dotenv').config(); 3 | 4 | // Connection details automatically pulled from environment variables 5 | // can be manually overriden here if needed 6 | const sql = postgres({}); 7 | 8 | const functions = { 9 | /** 10 | * Get all rows from a specified table. 11 | * @param {string} table - The name of the table to query. 12 | * @returns {Promise} - A promise that resolves to an array of rows. 13 | * @example 14 | * const users = await sql.functions.getRows('users'); 15 | */ 16 | async getRows(table) { 17 | return await sql`SELECT * FROM ${table}`; 18 | }, 19 | 20 | /** 21 | * Get a specific row from a specified table using a selector. 22 | * @param {string} table - The name of the table to query. 23 | * @param {Object} selector - An object representing the selection criteria. 24 | * @returns {Promise} - A promise that resolves to a single row. 25 | * @example 26 | * const user = await sql.functions.getRow('users', { id: 1 }); 27 | */ 28 | async getRow(table, selector) { 29 | return (await sql`SELECT * FROM ${table} WHERE ${sql(selector)}`); 30 | }, 31 | 32 | /** 33 | * Update a row in a specified table using a selector. 34 | * @param {string} table - The name of the table to update. 35 | * @param {Object} data - An object representing the data to update. 36 | * @param {Object} selector - An object representing the selection criteria. 37 | * @returns {Promise} - A promise that resolves to the result of the update operation. 38 | * @example 39 | * const result = await sql.functions.updateRow('users', { name: 'John Doe' }, { id: 1 }); 40 | */ 41 | async updateRow(table, data, selector) { 42 | return await sql`UPDATE ${table} SET ${sql(data)} WHERE ${sql(selector)}`; 43 | }, 44 | 45 | /** 46 | * Insert a row in a specified table 47 | * @param {string} table 48 | * @param {Object} data 49 | * @returns {Promise} 50 | * @example 51 | * const result = await sql.functions.insertRow('users', { name: 'John Doe', id: 1 }); 52 | */ 53 | async insertRow(table, data) { 54 | return await sql`INSERT INTO ${table} ${sql(data)}`; 55 | } 56 | } 57 | 58 | // monkey patch the pool with the functions object 59 | // not the best practice but it works for now 60 | sql.functions = functions; 61 | 62 | module.exports = sql; -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | 3 | const logger = createLogger({ 4 | level: 'info', 5 | format: format.combine( 6 | format.timestamp(), 7 | format.json() 8 | ), 9 | transports: [ 10 | new transports.Console(), 11 | new transports.File({ filename: 'error.log', level: 'error' }), 12 | new transports.File({ filename: 'combined.log' }) 13 | ], 14 | }); 15 | 16 | module.exports = logger; -------------------------------------------------------------------------------- /middlewares/authenticate.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | require('dotenv').config(); 3 | 4 | /** 5 | * Middleware to authenticate the user using it's JWT token 6 | * @param {Request} req 7 | * @param {Response} res 8 | * @param {NextFunction} next 9 | */ 10 | const authenticate = (req, res, next) => { 11 | const token = req.headers.authorization?.split(' ')[1]; 12 | if (token) { 13 | jwt.verify(token, process.env.JWT_SECRET || 'your_jwt_secret', (err, user) => { 14 | if (err) { 15 | return res.sendStatus(403); 16 | } 17 | req.user = user; 18 | next(); 19 | }); 20 | } else { 21 | res.sendStatus(401); 22 | } 23 | }; 24 | 25 | module.exports = authenticate; -------------------------------------------------------------------------------- /middlewares/disabled.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | /** 4 | * Middleware to disable the route 5 | * @param {Request} req 6 | * @param {Response} res 7 | * @param {NextFunction} next 8 | */ 9 | const disabled = (req, res, next) => { 10 | res.sendStatus(403); 11 | } 12 | 13 | module.exports = disabled; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "light-api", 3 | "version": "1.0.0", 4 | "description": "Lightweight express boilerplate", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "bcrypt": "^5.1.1", 14 | "bcryptjs": "^2.4.3", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.4.5", 17 | "express": "^4.19.2", 18 | "express-rate-limit": "^7.3.0", 19 | "joi": "^17.13.1", 20 | "jsonwebtoken": "^9.0.2", 21 | "mysql2": "^3.10.0", 22 | "postgres": "^3.4.4", 23 | "winston": "^3.13.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const sql = require('../db/db'); 4 | 5 | // Routes in this files are protected with the authenticate middleware 6 | 7 | /* 8 | curl -X GET http://localhost:5005/api/protected \ 9 | -H "Authorization: Bearer " 10 | */ 11 | 12 | router.get('/protected', (req, res) => { 13 | res.json({ message: 'This is a protected route', user: req.user }); 14 | }); 15 | 16 | /* 17 | curl -X GET http://localhost:5005/api/rows/users \ 18 | -H "Authorization: Bearer " 19 | */ 20 | router.get('/rows/:table', async (req, res) => { 21 | try { 22 | const rows = await sql.functions.getRows(req.params.table); 23 | res.json(rows); 24 | } catch (error) { 25 | res.status(500).json({ error: error.message }); 26 | } 27 | }); 28 | 29 | /* 30 | curl -X GET http://localhost:5005/api/row/users?id=1 31 | -H "Authorization: Bearer " 32 | */ 33 | router.get('/row/:table', async (req, res) => { 34 | try { 35 | const selector = req.query; 36 | const row = await sql.functions.getRow(req.params.table, selector); 37 | res.json(row); 38 | } catch (error) { 39 | res.status(500).json({ error: error.message }); 40 | } 41 | }); 42 | 43 | /* 44 | curl -X PUT http://localhost:5005/api/row/users \ 45 | -H "Content-Type: application/json" \ 46 | -d '{"data": {"username": "John Doe"}, "selector": {"id": 1}}' 47 | -H "Authorization Bearer " 48 | */ 49 | router.put('/row/:table', async (req, res) => { 50 | try { 51 | const data = req.body.data; 52 | const selector = req.body.selector; 53 | const result = await sql.functions.updateRow(req.params.table, data, selector); 54 | res.json(result); 55 | } catch (error) { 56 | res.status(500).json({ error: error.message }); 57 | } 58 | }); 59 | 60 | module.exports = router; -------------------------------------------------------------------------------- /routes/log.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const logger = require('../logger'); // Import the logger 4 | 5 | // This request will be logged to the combined.log file 6 | 7 | // curl -X GET http://localhost:5005/api/logger 8 | router.get('/', (req, res) => { 9 | logger.info('GET /log'); 10 | res.json({ message: 'The request has been logged in combined.log' }); 11 | }); 12 | 13 | module.exports = router; -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const userHandler = require('../classes/userHandler'); 4 | 5 | /* 6 | curl -X POST http://localhost:5005/users/register \ 7 | -H "Content-Type: application/json" \ 8 | -d '{ 9 | "username": "testuser", 10 | "password": "testpassword" 11 | }' 12 | */ 13 | router.post('/register', async (req, res) => { 14 | try { 15 | const { username, password } = req.body; 16 | const token = await userHandler.registerUser(username, password); 17 | res.status(201).json({ message: 'User registered successfully', token }); 18 | } catch (error) { 19 | res.status(500).json({ error: error.message }); 20 | } 21 | }); 22 | 23 | /* 24 | curl -X POST http://localhost:5005/users/login \ 25 | -H "Content-Type: application/json" \ 26 | -d '{ 27 | "username": "testuser", 28 | "password": "testpassword" 29 | }' 30 | */ 31 | router.post('/login', async (req, res) => { 32 | try { 33 | const { username, password } = req.body; 34 | const token = await userHandler.loginUser(username, password); 35 | res.json({ token }); 36 | } catch (error) { 37 | res.status(401).json({ error: error.message }); 38 | } 39 | }); 40 | 41 | module.exports = router; 42 | -------------------------------------------------------------------------------- /routes/validation.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Joi = require('joi'); 4 | 5 | const userSchema = Joi.object({ 6 | username: Joi.string().min(6).required(), 7 | }); 8 | 9 | // curl -X POST http://localhost:5005/validation/username 10 | router.post('/username', (req, res) => { 11 | const { error } = userSchema.validate(req.body); // req.body structure: { username: 'john' } 12 | if (error) { 13 | return res.status(400).json({ error: error.details[0].message }); 14 | } 15 | res.json({ message: 'Username is valid' }); 16 | }); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /runtime/runtime.js: -------------------------------------------------------------------------------- 1 | // This file provides a compatibility layer to support both Node and Bun runtimes 2 | 3 | //detect if bun is runtime, if false node is assumed as the runtime 4 | const bun = typeof Bun !== "undefined"; 5 | 6 | const bcrypt = require('bcryptjs'); 7 | const fs = require('fs').promises; 8 | 9 | 10 | const runtime = { 11 | 12 | /** 13 | * Hashes an inputted string 14 | * @param {String} input 15 | * @param {Number} iterations 16 | * @returns {Promise} hashed input 17 | */ 18 | async hash(input, iterations) { 19 | if (bun) { 20 | return await Bun.password.hash(input, { 21 | algorithm: "bcrypt", 22 | cost: iterations 23 | }) 24 | } 25 | return await bcrypt.hash(input, iterations); 26 | }, 27 | 28 | /** 29 | * Compares a given string value to a given hash 30 | * @param {String} s 31 | * @param {String} hash 32 | * @returns {Promise} matches 33 | */ 34 | async compareHash(s, hash) { 35 | if (bun) { 36 | return await Bun.password.verify(s, hash); // Bun will automatically detect the algorithm 37 | } 38 | return await bcrypt.compare(s, hash); 39 | }, 40 | 41 | /** 42 | * Reads in a file asynchronously as text 43 | * @param {String} filepath 44 | * @returns {Promise} file contents 45 | */ 46 | async readFileString(filepath) { 47 | if (bun) { 48 | const file = Bun.file(filepath); 49 | return await file.text(); 50 | } 51 | return await fs.readFile(filepath, 'utf-8'); 52 | } 53 | 54 | } 55 | 56 | module.exports = runtime; --------------------------------------------------------------------------------