From 4ecb4496b598dd25c7841f1216c438e41863456d Mon Sep 17 00:00:00 2001 From: rinaldo stevenazzi Date: Fri, 15 Aug 2025 17:28:18 +0200 Subject: [PATCH 1/3] WIP - add vercel configuration, improve typescript --- .gitignore | 5 ++- package.json | 11 +++--- src/api/index.ts | 3 ++ src/controllers/Global.ts | 13 ++++--- src/index.ts | 73 +++++++++++++++++++-------------------- tsconfig.json | 19 ++++------ vercel.json | 3 ++ 7 files changed, 64 insertions(+), 63 deletions(-) create mode 100644 src/api/index.ts create mode 100644 vercel.json diff --git a/.gitignore b/.gitignore index 84c69ff..ea1ffd1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ yarn-error.log .idea #env -.env \ No newline at end of file +.env + +dist/ +.vercel diff --git a/package.json b/package.json index b8e9824..e92da5e 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,15 @@ "name": "speed-puzzle-backend", "version": "0.0.1", "description": "speed puzzle backend", - "main": "dist/index.js", + "main": "src/api/index.ts", + "type": "module", "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/index.js", + "dev": "node src/api/index.ts", + "build": "node -p tsconfig.json", + "start": "node src/api/index.ts", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "echo \"Error: no test specified\" && exit 1", - "seed": "tsx src/scripts/seeder.ts" + "seed": "node src/scripts/seeder.ts" }, "dependencies": { "@faker-js/faker": "^9.9.0", diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..29085bb --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,3 @@ +// api/index.ts +import app from "../index.ts"; +export default app; diff --git a/src/controllers/Global.ts b/src/controllers/Global.ts index 2ab08d8..23383b7 100755 --- a/src/controllers/Global.ts +++ b/src/controllers/Global.ts @@ -1,7 +1,8 @@ -import MongoDB from "../services/MongoDB"; -import Users, { User } from "../services/Users"; -import Scores from "../services/Scores"; -import EVENTS from "../constants/events"; +import MongoDB from "../services/MongoDB.ts"; +import Users from "../services/Users.ts"; +import type { User } from "../services/Users.ts"; +import Scores from "../services/Scores.ts"; +import EVENTS from "../constants/events.ts"; import type { Db, ObjectId } from "mongodb"; // Results using literal event types @@ -134,9 +135,7 @@ export default class Global { /** * Top N scores with their associated user */ - async getTopScores( - limit = 10 - ): Promise< + async getTopScores(limit = 10): Promise< Array<{ score: number; user: { diff --git a/src/index.ts b/src/index.ts index eb4f928..d6cd891 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ -import express, { Request, Response } from "express"; +// src/index.ts +import express from "express"; import http from "http"; import "dotenv/config"; -import Global from "./controllers/Global"; -import EVENTS from "./constants/events"; +import Global from "./controllers/Global.ts"; +import EVENTS from "./constants/events.ts"; const app = express(); const server = http.createServer(app); - const PORT = Number(process.env.API_PORT) || 3000; const globalController = new Global(); @@ -18,36 +18,37 @@ globalController app.use(express.json()); app.get("/", (_req, res) => { + console.log("GET /"); res.send("

Hello world

"); }); -// --- Score check endpoint (schema-agnostic): checks against global min, does not persist --- app.post( "/score", - async (req: Request<{}, {}, { score: number }>, res: Response) => { + async ( + req: express.Request<{}, {}, { score: number }>, + res: express.Response + ) => { try { const { score } = req.body; const result = await globalController.checkScore(score); - if (result.message === EVENTS.SCORED) { - return res.status(200).send(); - } - return res.status(409).send(); + return result.message === EVENTS.SCORED + ? res.status(200).send() + : res.status(409).send(); } catch (e) { return res.status(406).send(e); } } ); -// --- Create user aligned with mobile schema: { userName, password, score? } --- app.post( "/adduser", async ( - req: Request< + req: express.Request< {}, {}, { userName: string; password: string; score?: number } >, - res: Response + res: express.Response ) => { try { const { userName, password, score } = req.body; @@ -56,49 +57,39 @@ app.post( password, score, }); - - if (result.message === EVENTS.USER_ALREADY_EXIST) { + if (result.message === EVENTS.USER_ALREADY_EXIST) return res.status(409).send("User Already Exist."); - } - if (result.message === EVENTS.USER_CREATED) { + if (result.message === EVENTS.USER_CREATED) return res.status(200).json(result.list); - } return res.send(); } catch (e) { - console.log("index - response 406"); return res.status(406).send(e); } } ); -// --- Add a score for a specific user --- app.post( "/users/:userName/scores", async ( - req: Request<{ userName: string }, {}, { value: number }>, - res: Response + req: express.Request<{ userName: string }, {}, { value: number }>, + res: express.Response ) => { try { const { userName } = req.params; const { value } = req.body; - - if (typeof value !== "number") { + if (typeof value !== "number") return res.status(400).send("value must be a number"); - } - const result = await globalController.addScoreForUser(userName, value); - if (result.message === EVENTS.SCORED && result.created) { - return res.status(201).json({ userId: result.userId, value }); - } - return res.status(409).send(); + return result.message === EVENTS.SCORED && result.created + ? res.status(201).json({ userId: result.userId, value }) + : res.status(409).send(); } catch (e) { return res.status(406).send(e); } } ); -// --- Get all users (public) --- -app.get("/users", async (_req: Request, res: Response) => { +app.get("/users", async (_req, res) => { try { const list = await globalController.listUsersPublic(); return res.status(200).json(list); @@ -107,17 +98,18 @@ app.get("/users", async (_req: Request, res: Response) => { } }); -// --- Get top N scores with user (default 10) --- app.get( "/scores/top", - async (req: Request<{}, {}, {}, { limit?: string }>, res: Response) => { + async ( + req: express.Request<{}, {}, {}, { limit?: string }>, + res: express.Response + ) => { try { const raw = req.query.limit; const parsed = raw ? parseInt(raw, 10) : 10; const limit = Number.isFinite(parsed) ? Math.min(Math.max(parsed, 1), 50) : 10; - const rows = await globalController.getTopScores(limit); return res.status(200).json(rows); } catch (e) { @@ -126,6 +118,11 @@ app.get( } ); -server.listen(PORT, () => { - console.log(`Listening on ${PORT}`); -}); +// ❗ run the HTTP listener only when NOT on Vercel +// if (!process.env.VERCEL_ENV) { +// console.log(`Listening on port ${PORT}`); +server.listen(PORT, () => console.log(`Listening on ${PORT}`)); +// } + +// ✅ expose the Express app for Vercel's /api entry +export default app; diff --git a/tsconfig.json b/tsconfig.json index e334007..475f80c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,10 @@ { "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "moduleResolution": "node", - "rootDir": "src", - "outDir": "dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true - }, - "include": ["src"], - "exclude": ["dist"] + "noEmit": true, // Optional - see note below + "target": "esnext", + "module": "nodenext", + "rewriteRelativeImportExtensions": true, + "erasableSyntaxOnly": true + // "verbatimModuleSyntax": true + } } diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..18da4e3 --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/api" }] +} From 19be992a6efad1eaaf4d389ca91ed7579d5c4eac Mon Sep 17 00:00:00 2001 From: rinaldo stevenazzi Date: Sat, 16 Aug 2025 13:05:17 +0200 Subject: [PATCH 2/3] fix db initialization to make the server run --- .next/trace | 2 + api/index.ts | 149 ++++++++++++++++++++++++++++++++++++++ package.json | 6 +- src/api/index.ts | 3 - src/controllers/Global.ts | 3 + src/index.ts | 3 +- src/services/MongoDB.ts | 8 +- tsconfig.json | 1 - vercel.json | 4 + 9 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 .next/trace create mode 100644 api/index.ts delete mode 100644 src/api/index.ts diff --git a/.next/trace b/.next/trace new file mode 100644 index 0000000..21b1bee --- /dev/null +++ b/.next/trace @@ -0,0 +1,2 @@ +[{"name":"next-dev","duration":375778,"timestamp":48743038978,"id":1,"tags":{},"startTime":1755273250317,"traceId":"67ad9cb661b8ac33"}] +[{"name":"generate-buildid","duration":97,"timestamp":48766044509,"id":4,"parentId":1,"tags":{},"startTime":1755273273323,"traceId":"6b2fe1b57754ff3a"},{"name":"load-custom-routes","duration":160,"timestamp":48766044646,"id":5,"parentId":1,"tags":{},"startTime":1755273273323,"traceId":"6b2fe1b57754ff3a"},{"name":"next-build","duration":47457,"timestamp":48765998214,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"15.4.6"},"startTime":1755273273276,"traceId":"6b2fe1b57754ff3a"}] diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..7817992 --- /dev/null +++ b/api/index.ts @@ -0,0 +1,149 @@ +// api/index.ts +import express from "express"; +import "dotenv/config"; +import Global from "../src/controllers/Global.ts"; +import EVENTS from "../src/constants/events.ts"; + +const app = express(); +app.use(express.json()); + +// --- Initialize DB on cold start and await before handling requests --- +const globalController = new Global(); +const ready = (async () => { + try { + await globalController.initDB(); + console.log("SERVER - initDB - DONE"); + } catch (e) { + console.error("SERVER - initDB - ERROR", e); + throw e; + } +})(); + +app.use(async (_req, _res, next) => { + try { + await ready; // wait for cold-start DB init once + next(); + } catch (e) { + next(e); + } +}); + +// --- Debug route (remove once stable) --- +app.get("/__debug", async (_req, res) => { + res.json({ + hasMongoURI: Boolean(process.env.MONGODB_URI), + vercelEnv: process.env.VERCEL_ENV || null, + nodeVersion: process.version, + }); +}); + +// --- Health/base route --- +app.get("/", async (_req, res) => { + console.log("GET /"); + res.send("

Hello world

"); +}); + +// --- Routes --- +app.post( + "/score", + async ( + req: express.Request<{}, {}, { score: number }>, + res: express.Response + ) => { + try { + const { score } = req.body; + const result = await globalController.checkScore(score); + return result.message === EVENTS.SCORED + ? res.status(200).send() + : res.status(409).send(); + } catch (e) { + console.error("POST /score error:", e); + return res.status(500).json({ error: "Internal error" }); + } + } +); + +app.post( + "/adduser", + async ( + req: express.Request< + {}, + {}, + { userName: string; password: string; score?: number } + >, + res: express.Response + ) => { + try { + const { userName, password, score } = req.body; + const result = await globalController.addUser({ + userName, + password, + score, + }); + if (result.message === EVENTS.USER_ALREADY_EXIST) + return res.status(409).send("User Already Exist."); + if (result.message === EVENTS.USER_CREATED) + return res.status(200).json(result.list); + return res.send(); + } catch (e) { + console.error("POST /adduser error:", e); + return res.status(500).json({ error: "Internal error" }); + } + } +); + +app.post( + "/users/:userName/scores", + async ( + req: express.Request<{ userName: string }, {}, { value: number }>, + res: express.Response + ) => { + try { + const { userName } = req.params; + const { value } = req.body; + if (typeof value !== "number") + return res.status(400).send("value must be a number"); + const result = await globalController.addScoreForUser(userName, value); + return result.message === EVENTS.SCORED && result.created + ? res.status(201).json({ userId: result.userId, value }) + : res.status(409).send(); + } catch (e) { + console.error("POST /users/:userName/scores error:", e); + return res.status(500).json({ error: "Internal error" }); + } + } +); + +app.get("/users", async (_req, res) => { + try { + const list = await globalController.listUsersPublic(); + return res.status(200).json(list); + } catch (e) { + console.error("GET /users error:", e); + return res.status(500).json({ error: "Internal error" }); + } +}); + +app.get( + "/scores/top", + async ( + req: express.Request<{}, {}, {}, { limit?: string }>, + res: express.Response + ) => { + try { + const raw = req.query.limit; + const parsed = raw ? parseInt(String(raw), 10) : 10; + const limit = Number.isFinite(parsed) + ? Math.min(Math.max(parsed, 1), 50) + : 10; + const rows = await globalController.getTopScores(limit); + return res.status(200).json(rows); + } catch (e) { + console.error("GET /scores/top error:", e); + return res.status(500).json({ error: "Internal error" }); + } + } +); + +// Do NOT call app.listen() on Vercel. Export the app for the Serverless Function. +export default app; diff --git a/package.json b/package.json index e92da5e..c5406bf 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "main": "src/api/index.ts", "type": "module", "scripts": { - "dev": "node src/api/index.ts", - "build": "node -p tsconfig.json", - "start": "node src/api/index.ts", + "dev": "node api/index.ts", + "build": "echo \"No build step for Vercel Functions\"", + "start": "node api/index.ts", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "echo \"Error: no test specified\" && exit 1", "seed": "node src/scripts/seeder.ts" diff --git a/src/api/index.ts b/src/api/index.ts deleted file mode 100644 index 29085bb..0000000 --- a/src/api/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// api/index.ts -import app from "../index.ts"; -export default app; diff --git a/src/controllers/Global.ts b/src/controllers/Global.ts index 23383b7..34dc613 100755 --- a/src/controllers/Global.ts +++ b/src/controllers/Global.ts @@ -29,9 +29,12 @@ export default class Global { async initDB(): Promise { try { const db = await this.mongoDB.connect(); + console.log("Global Controller - initDB initialisation successful"); this.db = db; this.users.init(db); + console.log("Global Controller - initDB users initialised"); this.scores.init(db); + console.log("Global Controller - initDB scores initialised"); } catch (e) { // eslint-disable-next-line no-console console.log("Global Controller - initDB initialisation failed !!!"); diff --git a/src/index.ts b/src/index.ts index d6cd891..80bc012 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,7 +120,8 @@ app.get( // ❗ run the HTTP listener only when NOT on Vercel // if (!process.env.VERCEL_ENV) { -// console.log(`Listening on port ${PORT}`); +console.log(`VERCEL_ENV : ${process.env.VERCEL_ENV}`); +console.log(`Listening on port ${PORT}`); server.listen(PORT, () => console.log(`Listening on ${PORT}`)); // } diff --git a/src/services/MongoDB.ts b/src/services/MongoDB.ts index a46c80c..9a1574c 100755 --- a/src/services/MongoDB.ts +++ b/src/services/MongoDB.ts @@ -11,7 +11,13 @@ export default class MongoDB { constructor() { // eslint-disable-next-line no-console console.log("MongoDB Class - Constructor"); - this.client = new MongoClient(uri); + this.client = new MongoClient(uri, { + serverSelectionTimeoutMS: 10000, // fail fast if unreachable + connectTimeoutMS: 10000, + maxPoolSize: 5, + retryWrites: true, + }); + console.log("MongoDB Class - client initialized", this.client); } async connect(): Promise { diff --git a/tsconfig.json b/tsconfig.json index 475f80c..087c6f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,5 @@ "module": "nodenext", "rewriteRelativeImportExtensions": true, "erasableSyntaxOnly": true - // "verbatimModuleSyntax": true } } diff --git a/vercel.json b/vercel.json index 18da4e3..08f2d3d 100644 --- a/vercel.json +++ b/vercel.json @@ -1,3 +1,7 @@ { + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": null, + "buildCommand": "", + "outputDirectory": null, "rewrites": [{ "source": "/(.*)", "destination": "/api" }] } From b782210153d8dc5c8ac81fcc874d2492cd19427d Mon Sep 17 00:00:00 2001 From: rinaldo stevenazzi Date: Mon, 18 Aug 2025 10:25:51 +0200 Subject: [PATCH 3/3] improve readme file, clean code --- README.md | 186 ++++++++++++++++++++++++++------------------------- api/index.ts | 1 - src/index.ts | 129 ----------------------------------- 3 files changed, 96 insertions(+), 220 deletions(-) delete mode 100644 src/index.ts diff --git a/README.md b/README.md index 5cfa816..64ebb91 100644 --- a/README.md +++ b/README.md @@ -4,84 +4,100 @@ Backend API for the **Speed Puzzle** app. - **Language**: TypeScript - **Framework**: Express (Node.js) -- **Database**: MongoDB -- **Data model** - - `users`: `{ _id, userName, password(hashed), createdAt, updatedAt }` - - `scores`: `{ _id, userId (ref users._id), value, createdAt }` +- **Runtime**: Vercel Functions (Node.js) +- **Database**: MongoDB Atlas --- -## Tech stack +## What changed (serverless-ready) -- Express (REST API) -- MongoDB Node.js driver -- TypeScript + `tsx` (watch/dev runner) -- `dotenv` for env vars -- `bcryptjs` for password hashing -- `@faker-js/faker` for seed data +This repo is now configured to run as **Vercel Serverless Functions**: + +- The Express app lives in **`/api/index.ts`** and **exports the app** (`export default app`), no `app.listen()`. +- On cold start, we **await DB initialization** before serving requests. +- API routes return **500** on internal errors (instead of 406) and log a helpful message. +- A diagnostic route `GET /__debug` is available during testing. --- ## Project structure ```text +api/ + index.ts # Express app exported for Vercel Functions src/ constants/events.ts controllers/Global.ts services/ - MongoDB.ts + MongoDB.ts # MongoDB client (Node driver) Users.ts Scores.ts - index.ts scripts/ seed.ts ``` -> Note: The seeder populates 15 users with 1–3 scores each (200–500). - --- -## Setup +## Environment variables -1. **Install** +Create these for **local dev** in a `.env` file at the project root and set the same keys in **Vercel → Project → Settings → Environment Variables** (Production scope): ```bash -npm install +MONGODB_URI="mongodb+srv://:@.mongodb.net/?retryWrites=true&w=majority&appName=" +API_PORT=3000 # used only for local non-serverless runs ``` -2. **Environment** — create a `.env` at the project root: +> For Vercel → Atlas connectivity, Atlas recommends allowing **`0.0.0.0/0`** (all IPs) because Vercel uses **dynamic egress IPs**. Tighten later with Secure Compute / private networking if required. citeturn0search2turn0search7 + +--- + +## Local development ```bash -MONGODB_URI="mongodb://localhost:27017" -API_PORT=3000 +npm install +npm run dev # uses `vercel dev` to emulate the platform ``` -3. **Run** +**Seed demo data** ```bash -# dev (watch) -npm run dev - -# prod -npm run build -npm start +npm run seed # inserts users + scores +npm run seed -- --reset # wipe & reseed ``` -4. **Seed demo data** +> The app runs as a Vercel Function locally; you don’t need `npm start` for serverless. + +--- + +## Deployment (Vercel) + +**One-time**: ```bash -# inserts 15 users with 1–3 scores each (values 200–500) -npm run seed +npm i -g vercel +vercel login +``` + +**Deploy**: -# wipe & reseed -npm run seed -- --reset +```bash +vercel # Preview deployment +vercel --prod # Production deployment ``` +**Vercel project settings** (for an API-only app): + +- **Framework Preset**: **Other** +- **Build Command**: _empty_ +- **Output Directory**: _empty_ + +This avoids the “Missing public directory” error that applies to static sites. If you previously set an Output Directory (e.g., `public/`), clear it. citeturn0search1 + --- ## REST API -**Base URL:** `http://localhost:3000` +**Base URL (prod):** `https://.vercel.app` ### GET `/` @@ -91,7 +107,13 @@ Health check. 200 OK → "

Hello world

" ``` ---- +### GET `/__debug` (temporary) + +Returns a few runtime facts to confirm env setup. + +```json +{ "hasMongoURI": true, "vercelEnv": "production", "nodeVersion": "v20.x" } +``` ### POST `/adduser` @@ -100,11 +122,7 @@ Create a user aligned with the mobile schema (optionally with an initial score). **Body** ```json -{ - "userName": "Ada Lovelace", - "password": "SeedUser#2025", - "score": 420 -} +{ "userName": "Ada Lovelace", "password": "SeedUser#2025", "score": 420 } ``` **Responses** @@ -112,16 +130,6 @@ Create a user aligned with the mobile schema (optionally with an initial score). - `200 OK` → array of users (public fields only) - `409 Conflict` → "User Already Exist." -**cURL** - -```bash -curl -s -X POST http://localhost:3000/adduser \ - -H 'content-type: application/json' \ - -d '{"userName":"Ada Lovelace","password":"SeedUser#2025","score":420}' | jq . -``` - ---- - ### POST `/users/:userName/scores` Add a score for an existing user. @@ -138,16 +146,6 @@ Add a score for an existing user. - `400 Bad Request` if `value` is not a number - `409 Conflict` if rejected by the acceptance rule -**cURL** - -```bash -curl -s -X POST 'http://localhost:3000/users/Ada%20Lovelace/scores' \ - -H 'content-type: application/json' \ - -d '{"value":451}' | jq . -``` - ---- - ### POST `/score` Check a raw score against the global minimum across all scores. **Does not persist**. @@ -163,16 +161,6 @@ Check a raw score against the global minimum across all scores. **Does not persi - `200 OK` if accepted - `409 Conflict` if rejected -**cURL** - -```bash -curl -i -X POST http://localhost:3000/score \ - -H 'content-type: application/json' \ - -d '{"score":300}' -``` - ---- - ### GET `/users` List all users (public fields only). @@ -190,14 +178,6 @@ List all users (public fields only). ] ``` -**cURL** - -```bash -curl -s http://localhost:3000/users | jq . -``` - ---- - ### GET `/scores/top?limit=10` Top `limit` scores (default 10). Each item returns `{ score, user }`. @@ -206,26 +186,52 @@ Top `limit` scores (default 10). Each item returns `{ score, user }`. ```json [ - { - "score": 497, - "user": { "_id": "665e...", "userName": "Noah Smith", "createdAt": 172..., "updatedAt": 172... } - } + { "score": 497, "user": { "_id": "665e...", "userName": "Noah Smith", "createdAt": 172..., "updatedAt": 172... } } ] ``` -**cURL** +--- -```bash -curl -s 'http://localhost:3000/scores/top?limit=5' | jq . -``` +## Serverless notes (important) + +- **Express on Vercel**: Files under `/api` become functions; export the Express app and let Vercel handle the server. Don’t call `app.listen()`. citeturn0search0turn0search3 +- **Cold start readiness**: We use an async `ready` promise to **await `initDB()`** on first request. +- **MongoDB Node driver**: `serverSelectionTimeoutMS` defaults to **30000ms**; we set shorter timeouts to fail fast during testing. citeturn0search14turn0search4 +- **Connection reuse**: Prefer a **singleton client** cached across invocations to avoid reconnect storms in serverless environments. (See `MongoDB.ts`.) --- -## Notes +## Troubleshooting + +### "Missing public directory" during deploy + +Your project is configured like a static site. For an API-only app, clear **Build Command** and **Output Directory** (Project → Settings). citeturn0search1 + +### `MongoServerSelectionError` / `ReplicaSetNoPrimary` / timeouts + +Usually network access to Atlas. Ensure Atlas Network Access allows your deployment to connect. For Vercel, allow **`0.0.0.0/0`** during testing (use strong creds), or adopt a fixed-egress solution for production. Also verify you use the **SRV** URI (`mongodb+srv://…`). citeturn0search2turn0search7 + +### Env variables not picked up + +Set them in **Vercel → Project → Settings → Environment Variables** (correct environment), then redeploy. + +### Seeing 406 on errors + +We now return **500** for server errors. If you still see 406, ensure your routes aren’t catching and rethrowing as 406. + +--- + +## Scripts + +```json +{ + "dev": "vercel dev", + "build": "echo \"No build step for Vercel Functions\"", + "seed": "tsx src/scripts/seed.ts" +} +``` -- Passwords are hashed with `bcryptjs` (10 rounds). -- Seeding uses Faker’s `person.fullName()` to generate human names. -- `tsx` provides fast TypeScript execution with watch mode for development. +> Vercel builds TypeScript in `/api` automatically; a real `build` step isn’t required for this API-only project. citeturn0search3 --- diff --git a/api/index.ts b/api/index.ts index 7817992..ed9936d 100644 --- a/api/index.ts +++ b/api/index.ts @@ -145,5 +145,4 @@ app.get( } ); -// Do NOT call app.listen() on Vercel. Export the app for the Serverless Function. export default app; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 80bc012..0000000 --- a/src/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -// src/index.ts -import express from "express"; -import http from "http"; -import "dotenv/config"; -import Global from "./controllers/Global.ts"; -import EVENTS from "./constants/events.ts"; - -const app = express(); -const server = http.createServer(app); -const PORT = Number(process.env.API_PORT) || 3000; - -const globalController = new Global(); -globalController - .initDB() - .then(() => console.log("SERVER - initDB - DONE")) - .catch((e) => console.log("SERVER - initDB - ERROR", e)); - -app.use(express.json()); - -app.get("/", (_req, res) => { - console.log("GET /"); - res.send("

Hello world

"); -}); - -app.post( - "/score", - async ( - req: express.Request<{}, {}, { score: number }>, - res: express.Response - ) => { - try { - const { score } = req.body; - const result = await globalController.checkScore(score); - return result.message === EVENTS.SCORED - ? res.status(200).send() - : res.status(409).send(); - } catch (e) { - return res.status(406).send(e); - } - } -); - -app.post( - "/adduser", - async ( - req: express.Request< - {}, - {}, - { userName: string; password: string; score?: number } - >, - res: express.Response - ) => { - try { - const { userName, password, score } = req.body; - const result = await globalController.addUser({ - userName, - password, - score, - }); - if (result.message === EVENTS.USER_ALREADY_EXIST) - return res.status(409).send("User Already Exist."); - if (result.message === EVENTS.USER_CREATED) - return res.status(200).json(result.list); - return res.send(); - } catch (e) { - return res.status(406).send(e); - } - } -); - -app.post( - "/users/:userName/scores", - async ( - req: express.Request<{ userName: string }, {}, { value: number }>, - res: express.Response - ) => { - try { - const { userName } = req.params; - const { value } = req.body; - if (typeof value !== "number") - return res.status(400).send("value must be a number"); - const result = await globalController.addScoreForUser(userName, value); - return result.message === EVENTS.SCORED && result.created - ? res.status(201).json({ userId: result.userId, value }) - : res.status(409).send(); - } catch (e) { - return res.status(406).send(e); - } - } -); - -app.get("/users", async (_req, res) => { - try { - const list = await globalController.listUsersPublic(); - return res.status(200).json(list); - } catch (e) { - return res.status(406).send(e); - } -}); - -app.get( - "/scores/top", - async ( - req: express.Request<{}, {}, {}, { limit?: string }>, - res: express.Response - ) => { - try { - const raw = req.query.limit; - const parsed = raw ? parseInt(raw, 10) : 10; - const limit = Number.isFinite(parsed) - ? Math.min(Math.max(parsed, 1), 50) - : 10; - const rows = await globalController.getTopScores(limit); - return res.status(200).json(rows); - } catch (e) { - return res.status(406).send(e); - } - } -); - -// ❗ run the HTTP listener only when NOT on Vercel -// if (!process.env.VERCEL_ENV) { -console.log(`VERCEL_ENV : ${process.env.VERCEL_ENV}`); -console.log(`Listening on port ${PORT}`); -server.listen(PORT, () => console.log(`Listening on ${PORT}`)); -// } - -// ✅ expose the Express app for Vercel's /api entry -export default app;