Copy these templates when scaffolding. Adjust names/paths to the target project.
{
"name": "backend",
"version": "1.0.0",
"type": "module",
"packageManager": "pnpm@10.26.1",
"scripts": {
"dev": "nodemon --watch src --ext ts --exec \"tsx src/index.ts\"",
"build": "tsc -b",
"start": "node dist/src/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev"
},
"dependencies": {
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"http-status": "^2.1.0",
"nodemon": "^3.1.14",
"pg": "^8.21.0",
"tsx": "^4.22.4",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^25.9.2",
"@types/pg": "^8.20.0",
"prisma": "^7.8.0",
"typescript": "^6.0.3"
}
}{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}
model SystemPing {
id Int @id @default(autoincrement())
message String
createdAt DateTime @default(now())
@@map("system_pings")
}import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});import express from "express";
import "dotenv/config";
import cors from "cors";
import routes from "./routes/index.js";
const app = express();
app.set("trust proxy", true);
app.use(
cors({
origin: process.env.FRONTEND_URL ?? "http://localhost:3000",
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
credentials: true,
}),
);
app.use(express.json());
app.use("/api/v1", routes);
const { PORT } = process.env;
app.listen(Number(PORT ?? 4000), () => {
console.log(`Server running on http://localhost:${PORT ?? 4000}`);
});import express, { Router } from "express";
import testRoute from "./test.route.js";
const router: Router = express.Router();
const defaultRoutes = [
{ path: "/test", route: testRoute },
];
defaultRoutes.forEach(({ path, route }) => {
router.use(path, route);
});
export default router;import express, { Router } from "express";
import testController from "../controllers/test.controller.js";
const router: Router = express.Router();
router.get("/loggerAPI", testController.loggerFunction);
export default router;import { Request, Response } from "express";
import httpStatus from "http-status";
import { response } from "../utils/reponses.js";
import testService from "../services/test.service.js";
import ApiError from "../utils/api-error.js";
const loggerFunction = async (req: Request, res: Response) => {
try {
const answer = await testService.logger();
return response(res, httpStatus.OK, "All Okay", answer);
} catch (error) {
if (error instanceof ApiError) {
return response(res, error.statusCode, error.message, null);
}
console.error("[test.loggerFunction]", error);
return response(
res,
httpStatus.INTERNAL_SERVER_ERROR,
"Internal server error.",
null,
);
}
};
export default {
loggerFunction,
};import httpStatus from "http-status";
import { prisma } from "../lib/prisma.js";
import ApiError from "../utils/api-error.js";
const logger = async () => {
const message = "This Works Test 3 ";
try {
await prisma.systemPing.create({ data: { message } });
} catch (error) {
console.error("[test.logger] DB write failed:", error);
throw new ApiError(
httpStatus.SERVICE_UNAVAILABLE,
"Database unavailable.",
);
}
return message;
};
export default {
logger,
};import { z } from "zod";
// Add schemas here as features grow. Example:
// const listItems = z.object({
// query: z.object({
// page: z.coerce.number().int().positive().optional().default(1),
// }),
// });
export default {};import { ZodObject, ZodError } from "zod";
import { Request, Response, NextFunction } from "express";
export const validate =
(schema: ZodObject<any>) =>
(req: Request, res: Response, next: NextFunction) => {
try {
schema.parse({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (err) {
if (err instanceof ZodError) {
return res.status(400).json({
success: false,
message: "Validation Error",
errors: err.issues.map((issue) => issue.message),
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error - Zod",
});
}
};// TODO: Wire to Better Auth / JWT — replace stub implementation.
import { NextFunction, Request, Response } from "express";
export interface UserPayload {
id: string;
email: string;
name: string;
role: string;
isActive: boolean;
}
export const isAuthenticated = async (
req: Request,
res: Response,
next: NextFunction,
) => {
// STUB: Remove this block when wiring real auth.
if (process.env.AUTH_STUB_ENABLED === "true") {
req.user = {
id: "stub-user-id",
email: "stub@example.com",
name: "Stub User",
role: "ADMIN",
isActive: true,
};
return next();
}
return res.status(401).json({ error: "Unauthorized" });
};
export const requireRole = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Unauthorized" });
}
if (!roles.includes((req.user as UserPayload).role)) {
return res.status(403).json({ error: "Forbidden: insufficient role." });
}
next();
};
};import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../../generated/prisma/client.js";
const connectionString = process.env.DATABASE_URL!;
const adapter = new PrismaPg({ connectionString });
const prisma = new PrismaClient({ adapter });
export { prisma };class ApiError extends Error {
statusCode: number;
isOperational: boolean;
constructor(statusCode: number, message: string, options?: ErrorOptions) {
super(message, options);
this.statusCode = statusCode;
this.isOperational = true;
Object.setPrototypeOf(this, new.target.prototype);
}
}
export default ApiError;import { Response } from "express";
import httpStatus from "http-status";
const response = (
res: Response,
statusCode: number,
message: string,
data = {} as any,
) => {
return res.status(statusCode).json({
message,
data,
});
};
const internalServerError = (res: Response, message: string) => {
return response(res, httpStatus.INTERNAL_SERVER_ERROR, message);
};
export { internalServerError, response };import { UserPayload } from "../../middlewares/auth-middleware.js";
declare global {
namespace Express {
interface Request {
user?: UserPayload;
}
}
}
export {};PORT=4000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
FRONTEND_URL=http://localhost:3000
AUTH_STUB_ENABLED=true
node_modules/
dist/
generated/
.env
Use these when extending an existing project (Mode 2).
import express, { Router } from "express";
import {
isAuthenticated,
requireRole,
} from "../middlewares/auth-middleware.js";
import { validate } from "../middlewares/validate.js";
import productController from "../controllers/product.controller.js";
import productValidator from "../validators/product.validator.js";
const router: Router = express.Router();
// POST /api/v1/products
router.post(
"/",
isAuthenticated,
requireRole("ADMIN"),
validate(productValidator.createProduct),
productController.create,
);
// GET /api/v1/products
router.get(
"/",
isAuthenticated,
requireRole("ADMIN"),
validate(productValidator.listProducts),
productController.list,
);
// GET /api/v1/products/:id
router.get(
"/:id",
isAuthenticated,
validate(productValidator.getProductById),
productController.getById,
);
export default router;import { z } from "zod";
const createProduct = z.object({
body: z.object({
name: z.string().min(1, { message: "name is required" }),
price: z.coerce.number().positive({ message: "price must be positive" }),
}),
});
const listProducts = z.object({
query: z.object({
page: z.coerce.number().int().positive().optional().default(1),
limit: z.coerce.number().int().positive().max(100).optional().default(20),
}),
});
const getProductById = z.object({
params: z.object({
id: z.coerce
.number({ message: "id must be a valid number" })
.int({ message: "id must be an integer" })
.positive({ message: "id must be a positive number" }),
}),
});
export default {
createProduct,
listProducts,
getProductById,
};import httpStatus from "http-status";
import { prisma } from "../lib/prisma.js";
import ApiError from "../utils/api-error.js";
interface CreateProductInput {
name: string;
price: number;
}
const createProduct = async (input: CreateProductInput) => {
return prisma.product.create({ data: input });
};
const listProducts = async (page: number, limit: number) => {
const skip = (page - 1) * limit;
const [items, total] = await prisma.$transaction([
prisma.product.findMany({ skip, take: limit, orderBy: { createdAt: "desc" } }),
prisma.product.count(),
]);
return { items, total, page, limit };
};
const getProductById = async (id: number) => {
const product = await prisma.product.findUnique({ where: { id } });
if (!product) {
throw new ApiError(httpStatus.NOT_FOUND, "Product not found.");
}
return product;
};
export default {
createProduct,
listProducts,
getProductById,
};import { Request, Response } from "express";
import httpStatus from "http-status";
import { response } from "../utils/reponses.js";
import ApiError from "../utils/api-error.js";
import productService from "../services/product.service.js";
const create = async (req: Request, res: Response) => {
try {
const { name, price } = req.body;
const product = await productService.createProduct({ name, price });
return response(res, httpStatus.CREATED, "Product created successfully.", product);
} catch (error) {
if (error instanceof ApiError) {
return response(res, error.statusCode, error.message, null);
}
console.error("[product.create]", error);
return response(res, httpStatus.INTERNAL_SERVER_ERROR, "Internal server error.", null);
}
};
const list = async (req: Request, res: Response) => {
try {
const page = Number(req.query.page ?? 1);
const limit = Number(req.query.limit ?? 20);
const result = await productService.listProducts(page, limit);
return response(res, httpStatus.OK, "Products fetched successfully.", result);
} catch (error) {
if (error instanceof ApiError) {
return response(res, error.statusCode, error.message, null);
}
console.error("[product.list]", error);
return response(res, httpStatus.INTERNAL_SERVER_ERROR, "Internal server error.", null);
}
};
const getById = async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const product = await productService.getProductById(id);
return response(res, httpStatus.OK, "Product fetched successfully.", product);
} catch (error) {
if (error instanceof ApiError) {
return response(res, error.statusCode, error.message, null);
}
console.error("[product.getById]", error);
return response(res, httpStatus.INTERNAL_SERVER_ERROR, "Internal server error.", null);
}
};
export default {
create,
list,
getById,
};import productRoute from "./product.route.js";
const defaultRoutes = [
{ path: "/test", route: testRoute },
{ path: "/products", route: productRoute }, // add this line
];When product.route.ts already exists, only append to each file:
validator — add schema + export:
const updateProduct = z.object({
params: z.object({ id: z.coerce.number().int().positive() }),
body: z.object({
name: z.string().min(1).optional(),
price: z.coerce.number().positive().optional(),
}).refine((b) => b.name !== undefined || b.price !== undefined, {
message: "At least one field is required",
}),
});
// add updateProduct to export default { ... }service — add function + export:
const updateProduct = async (id: number, data: { name?: string; price?: number }) => {
await getProductById(id); // reuse 404 check
return prisma.product.update({ where: { id }, data });
};controller — add handler + export:
const update = async (req: Request, res: Response) => { /* same try/catch pattern */ };route — add route line (PATCH before DELETE if both use /:id):
router.patch("/:id", isAuthenticated, requireRole("ADMIN"), validate(productValidator.updateProduct), productController.update);Omit isAuthenticated and requireRole:
router.get("/health", healthController.check);Always use .refine() to require at least one field on partial updates:
body: z.object({
fieldA: z.string().optional(),
fieldB: z.number().optional(),
}).strict().refine(
(b) => Object.values(b).some((v) => v !== undefined),
{ message: "At least one field is required" },
),const getItemById = z.object({
params: z.object({
id: z.coerce
.number({ message: "id must be a valid number" })
.int({ message: "id must be an integer" })
.positive({ message: "id must be a positive number" }),
}),
});
export default { getItemById };router.get(
"/:id",
isAuthenticated,
requireRole("ADMIN"),
validate(itemValidator.getItemById),
itemController.getById,
);const getItemById = async (id: number) => {
const item = await prisma.item.findUnique({ where: { id } });
if (!item) {
throw new ApiError(httpStatus.NOT_FOUND, "Item not found.");
}
return item;
};const getById = async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const item = await itemService.getItemById(id);
return response(res, httpStatus.OK, "Item fetched successfully.", item);
} catch (error) {
if (error instanceof ApiError) {
return response(res, error.statusCode, error.message, null);
}
console.error("[item.getById]", error);
return response(res, httpStatus.INTERNAL_SERVER_ERROR, "Internal server error.", null);
}
};