Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/templates/ts/mysql/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
dist/
.env
.env.*
coverage/
*.log
8 changes: 8 additions & 0 deletions src/templates/ts/mysql/default.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PORT=3000
JWT_SECRET=your_jwt_secret
JWT_EXPIRES_IN=1d
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=
DB_DATABASE=test
DB_CONNECTION_LIMIT=10
36 changes: 36 additions & 0 deletions src/templates/ts/mysql/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "ts-mysql",
"version": "1.0.0",
"description": "TypeScript Express API template with MySQL (mysql2)",
"type": "module",
"main": "dist/app.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/app.ts",
"build": "tsc",
"start": "node dist/app.js",
"lint": "eslint . --ext .ts",
"format": "prettier --write ."
},
"dependencies": {
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"http-errors": "^2.0.0",
"jsonwebtoken": "^9.0.3",
"mysql2": "^3.6.0",
"morgan": "^1.10.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^18.0.0",
"@types/jsonwebtoken": "^9.0.2",
"@types/bcryptjs": "^2.4.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0",
"eslint": "^9.0.0",
"eslint-plugin-import": "^2.29.1",
"nodemon": "^3.1.14",
"prettier": "^3.2.5"
}
}
47 changes: 47 additions & 0 deletions src/templates/ts/mysql/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import express, { Request, Response, NextFunction } from "express";
import morgan from "morgan";
import createError from "http-errors";
import cors from "cors";
import dotenv from "dotenv";
import apiRoutes from "./routes/user.route";
import authRoutes from "./routes/auth.route";
import { connectDB } from "./configs/db";
import { ensureUsersTable } from "./models/user.model";

dotenv.config();

await connectDB();
await ensureUsersTable();

const app = express();

// Middlewares
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(morgan("dev"));

// Routes
app.get("/", (_req: Request, res: Response) =>
res.json({ message: "API is running 🚀" })
);
app.use("/api", apiRoutes);
app.use("/api/auth", authRoutes);

// 404 handler
app.use((_req: Request, _res: Response, next: NextFunction) => {
next(createError.NotFound());
});

// Global error handler
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
res.status(err.status || 500).json({
success: false,
message: err.message || "Internal Server Error",
});
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =>
console.log(`🚀 Server running @ http://localhost:${PORT}`)
);
13 changes: 13 additions & 0 deletions src/templates/ts/mysql/src/configs/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import dotenv from "dotenv";

dotenv.config();

export default {
db: {
host: process.env.DB_HOST || "localhost",
user: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD || "",
database: process.env.DB_DATABASE || "test",
connectionLimit: Number(process.env.DB_CONNECTION_LIMIT) || 10,
},
};
50 changes: 50 additions & 0 deletions src/templates/ts/mysql/src/configs/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import mysql, { Pool } from "mysql2/promise";
import config from "./config";

let pool: Pool | null = null;

export const connectDB = async (): Promise<void> => {
try {
pool = mysql.createPool({
host: config.db.host,
user: config.db.user,
password: config.db.password,
database: config.db.database,
waitForConnections: true,
connectionLimit: config.db.connectionLimit || 10,
queueLimit: 0,
});

const connection = await pool.getConnection();
try {
const [rows] = await connection.query("SELECT 1 + 1 AS solution");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
console.log("The solution is:", (rows as any)[0].solution);
} finally {
connection.release();
}

console.log(`✅ MysqlDB Connected: ${config.db.host}`);
} catch (error: any) {
console.error("❌ MysqlDB connection error:", error.message);
process.exit(1);
}
};

export const getPool = (): Pool => {
if (!pool) {
throw new Error("Pool not initialized. Call connectDB() first.");
}
return pool;
};

export const query = async (sql: string, params: any[] = []): Promise<any> => {
const p = getPool();
try {
const [rows] = await p.query(sql, params);
return rows;
} catch (error: any) {
console.error("❌ Query error:", error.message);
throw error;
}
};
92 changes: 92 additions & 0 deletions src/templates/ts/mysql/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import asyncHandler from "../utils/asyncHandler";
import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";
import { successResponse, errorResponse } from "../utils/response";
import * as userRepo from "../repositories/user.repository";
import type { Request, Response } from "express";

export const login = asyncHandler(async (req: Request, res: Response) => {
try {
const { email, password } = req.body as {
email?: string;
password?: string;
};

if (!email || !password) {
return res
.status(400)
.json(errorResponse({ status: 400 }, "Email and password required"));
}

const user = await userRepo.getUserByEmail(email);
if (!user) {
return res
.status(401)
.json(errorResponse({ status: 401 }, "Invalid credentials"));
}

const isMatch = await bcrypt.compare(password, user.password || "");
if (!isMatch) {
return res
.status(401)
.json(errorResponse({ status: 401 }, "Invalid credentials"));
}

const token = jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET as string,
{ expiresIn: process.env.JWT_EXPIRES_IN || "1d" }
);

res.json(successResponse({ token }, "Login successful"));
} catch (error) {
res.status(500).json(errorResponse(error, "Login failed"));
}
});

export const signup = asyncHandler(async (req: Request, res: Response) => {
try {
const { name, email, password, age } = req.body as {
name?: string;
email?: string;
password?: string;
age?: number;
};

if (!name || !email || !password) {
return res
.status(400)
.json(
errorResponse({ status: 400 }, "Name, email, and password required")
);
}

const existingUser = await userRepo.getUserByEmail(email);
if (existingUser) {
return res
.status(400)
.json(errorResponse({ status: 400 }, "User already exists"));
}

const hashedPassword = await bcrypt.hash(password, 10);

const newUser = await userRepo.createUser({
name,
email,
password: hashedPassword,
age: age || 0,
});

const token = jwt.sign(
{ id: newUser.id, email: newUser.email },
process.env.JWT_SECRET as string,
{ expiresIn: process.env.JWT_EXPIRES_IN || "1d" }
);

res
.status(201)
.json(successResponse({ token, user: newUser }, "Signup successful"));
} catch (error) {
res.status(500).json(errorResponse(error, "Signup failed"));
}
});
66 changes: 66 additions & 0 deletions src/templates/ts/mysql/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import asyncHandler from "../utils/asyncHandler";
import * as userService from "../services/user.service";
import { successResponse, errorResponse } from "../utils/response";
import type { Request, Response } from "express";

export const getUsers = asyncHandler(async (_req: Request, res: Response) => {
try {
const users = await userService.getAllUsers();
res.json(successResponse(users));
} catch (error) {
res.status(500).json(errorResponse(error, "Failed to fetch users"));
}
});

export const getUser = asyncHandler(async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const user = await userService.getUserById(id);
if (!user)
return res
.status(404)
.json(errorResponse({ status: 404 }, "User not found"));
res.json(successResponse(user));
} catch (error) {
res.status(500).json(errorResponse(error, "Failed to fetch user"));
}
});

export const createUser = asyncHandler(async (req: Request, res: Response) => {
try {
const data = req.body;
const user = await userService.createUser(data);
res.status(201).json(successResponse(user, "User created"));
} catch (error) {
res.status(500).json(errorResponse(error, "Failed to create user"));
}
});

export const updateUser = asyncHandler(async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const data = req.body;
const updated = await userService.updateUser(id, data);
if (!updated)
return res
.status(404)
.json(errorResponse({ status: 404 }, "User not found"));
res.json(successResponse(updated, "User updated"));
} catch (error) {
res.status(500).json(errorResponse(error, "Failed to update user"));
}
});

export const deleteUser = asyncHandler(async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const deleted = await userService.deleteUser(id);
if (!deleted)
return res
.status(404)
.json(errorResponse({ status: 404 }, "User not found"));
res.json(successResponse(deleted, "User deleted"));
} catch (error) {
res.status(500).json(errorResponse(error, "Failed to delete user"));
}
});
19 changes: 19 additions & 0 deletions src/templates/ts/mysql/src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

export const protect = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ success: false, message: "Not authorized" });
}

const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as any;
// attach user info to req if needed
(req as any).user = { id: decoded.id, email: decoded.email };
next();
} catch (error) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
};
23 changes: 23 additions & 0 deletions src/templates/ts/mysql/src/models/user.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { query } from "../configs/db";
import type { User } from "../types/types";

export const ensureUsersTable = async (): Promise<void> => {
const sql = `
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
age INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
`;
try {
await query(sql);
console.log("✅ Users table ensured");
} catch (error: any) {
console.error("❌ Error ensuring users table:", error.message);
throw error;
}
};
Loading