A small full‑stack app to upload files, generate QR codes for public view links, and manage files (list/delete).
Authentication uses JWT in HttpOnly cookies so that the frontend can stay stateless while the backend enforces sessions. Built for learning + interview demo.
Live backend:
https://qr-code-generator-0opz.onrender.com
API base:https://qr-code-generator-0opz.onrender.com/api
Local frontend (Vite):http://localhost:5173(during development)
Frontend
- React + Vite
- React Router
- Redux Toolkit (Auth + Files slices)
- Tailwind + shadcn/ui components
Backend
- Node.js + Express
- MongoDB (Atlas) via Mongoose
- Multer (file uploads)
qrcode(QR PNG generation)jsonwebtoken(JWT)cookie-parser,cors
Infra / Deploy
- Render (free tier) for Node service
- Optional: ngrok / localtunnel for sharing local server during development
repo/
├─ client/ # Vite React app
│ ├─ src/
│ │ ├─ Features/
│ │ │ ├─ auth/AuthSlice.jsx
│ │ │ └─ files/filesSlice.jsx
│ │ ├─ Pages/ (Login, Signup, Dashboard)
│ │ ├─ BrowseRoute.jsx
│ │ ├─ lib/apiClient.js
│ │ └─ App.jsx, main.jsx
│ └─ index.html, package.json, .env
└─ server/ # Express API
├─ src/
│ ├─ index.js
│ ├─ routes.auth.js
│ ├─ auth.controller.js, auth.service.js, utils.jwt.js, users.memory.js
│ ├─ files/
│ │ ├─ file.routes.js
│ │ ├─ file.controller.js
│ │ ├─ file.service.js
│ │ ├─ file.model.js
│ │ ├─ upload.middleware.js
│ │ ├─ viewer.routes.js # GET /v/:id (simple HTML viewer)
│ │ ├─ serve.routes.js # public file serve if needed
│ │ ├─ qr.routes.js # GET /qr/:id.png
│ │ └─ qr.controller.js # reads QR from disk and sends PNG
│ └─ db/mongo.js
└─ package.json, .env
- Node 18+ (works on 22.x too)
- A MongoDB URI (Atlas free cluster is fine)
cd server- Create
.env(see Environment Variables below). Minimal example:PORT=8000 PUBLIC_BASE_URL=http://localhost:8000 VITE_ORIGIN=http://localhost:5173 MONGO_URI=mongodb+srv://<user>:<pass>@cluster0.xxxxx.mongodb.net/ MONGO_DB=qr_app JWT_SECRET=some-long-random-secret
- Install & run:
npm install npm start # runs node src/index.js # (optional for DX) npm run dev # if you add nodemon
- Health check: open
http://localhost:8000/health→ should return ok.
cd client- Create
.env:VITE_API_BASE_URL=http://localhost:8000/api
- Install & run:
npm install npm run dev
- Open
http://localhost:5173in the browser.
Important (cookies + CORS):
- Frontend must call
fetch(..., { credentials: "include" }).- Server must set
app.use(cors({ origin: VITE_ORIGIN, credentials: true })).- Cookies are set with
SameSite=None; Securein production (HTTPS).
| Name | Example | Why it matters |
|---|---|---|
PORT |
8000 |
Server port |
PUBLIC_BASE_URL |
https://qr-code-generator-0opz.onrender.com |
Used to create absolute QR/view URLs |
VITE_ORIGIN |
http://localhost:5173 or your deployed frontend |
CORS allow‑list origin |
MONGO_URI |
mongodb+srv://... |
MongoDB connection string |
MONGO_DB |
qr_app |
DB name |
JWT_SECRET |
long random string | JWT signing |
Client .env
VITE_API_BASE_URL=https://qr-code-generator-0opz.onrender.com/api # or local
Auth
POST /api/signup→{ email, password }→ setssessioncookie, returns{ user }POST /api/login→{ email, password }→ sets cookie, returns{ user }GET /api/me→ returns{ user }(readssessionfrom cookie)POST /api/logout→ clears cookie
Files
POST /api/files(multipart/form-data; field:files) → uploads file(s), creates QR, returns{ items: [...] }GET /api/files?page=1&limit=20→ list current user's filesGET /api/files/:id→ single file metaDELETE /api/files/:id→ delete one file + its QR (if implemented withauthRequired)
Public / QR
GET /qr/:id.png→ QR code PNG (forced download viaContent-Dispositionheader)GET /v/:id→ simple view page that renders/serves the stored file
- Signup/Login
- Client calls
/api/signupor/api/loginwith JSON. - Server validates and creates JWT → sets
sessioncookie (HttpOnly,SameSite=None,Securein prod). - Client stores nothing sensitive; state is in Redux from
/api/meresponse.
- Session check
- On app load, client calls
/api/me. - If cookie is valid, server returns
{ user }; otherwise401. - Protected routes render only when
isLoggedInis true.
- Upload & QR
- Client sends multipart
files[]to/api/files. - Server writes files to disk (Multer), generates QR PNG using the public view URL (
/v/:id) and stores QR path + meta in Mongo. - Client lists
/api/filesand displaysqrPngUrlin grid.
- Delete
- Client calls
DELETE /api/files/:id. - Server removes Mongo doc and deletes QR PNG (and the uploaded file) from disk.
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": {
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.0",
"multer": "^1.4.5",
"qrcode": "^1.5.3"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 5173"
}
}If any of these scripts are missing in your repo, you can add them as above.
- Users storage (demo): Users are stored in memory (
users.memory.js) and passwords are plain text for simplicity.- Real app: Store hashed passwords (bcrypt/argon2) in MongoDB with unique email index.
- Disk storage: Uploaded files & QR PNGs are saved to local disk. On Render free tier, disk is ephemeral (a fresh deploy resets files).
- Real app: Store files in S3 / Cloud Storage or Mongo GridFS. Store only metadata in Mongo.
- Cookies & CORS: Cross‑origin cookies require
SameSite=None; Secureand HTTPS. We set CORS withcredentials: true. - Single user quota/rules: No rate limits, no file type allow‑list beyond basic size limit.
- Real app: Add size/type validation, antivirus scan, rate limiting, and audit logs.
- Public view link:
/v/:idserves the uploaded file. If you need expiring links, generate signed URLs instead of static IDs.
- Push server code to GitHub.
- Create a Render → Web Service → pick your repo.
- Set Root Directory =
server - Build Command =
npm install - Start Command =
npm start - Add Environment Variables (same as local, but
PUBLIC_BASE_URLmust be the Render URL). - After deploy: health check
/health. - Update
client/.env→VITE_API_BASE_URL=https://<your-render-app>.onrender.com/apiand rebuild frontend if deploying it somewhere.
Note: Disk on Render free tier is not persistent across deploys. For durable files, use S3/GridFS.
- 401 from
/api/me:- Client must use
credentials: 'include'in fetch (ourapiClientdoes this). - Server CORS must have
credentials: trueand correctorigin. - Cookies require HTTPS with
SameSite=None; Securein production.
- Client must use
- QR not visible on phone: Use a public base URL (
PUBLIC_BASE_URL) instead ofhttp://localhost:8000so QR encodes a reachable link. - "Cannot POST /signup": The client must call
/api/signup(note the/apiprefix). Make sureVITE_API_BASE_URLends with/api.
Login → set cookie → /api/me loads user → Dashboard shows Files Grid
Upload → /api/files → DB row created + QR PNG saved → Grid shows qrPngUrl
Delete → /api/files/:id → remove doc + delete files → grid updates
- Route (
routes.auth.js,files/file.routes.js): URL mapping → which controller function runs. - Controller (
auth.controller.js,files/file.controller.js): HTTP layer; parses input, calls services, shapes HTTP responses. - Service (
auth.service.js,files/file.service.js): Business logic; DB/FS calls, QR generation, DTO mapping. - Model (
files/file.model.js): Mongoose schema for file metadata (name, mime, size, qr paths, uploaderId). - Middleware (
upload.middleware.js,authRequired.js): Cross‑cutting concerns (Multer for uploads, auth guard).
This separation keeps code testable, readable, and replaceable (e.g., swapping disk‑storage → S3 only touches service layer).
-
Manual API test with curl/postman:
# Signup curl -i -X POST https://<render>/api/signup \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"123456"}' # Me curl -i https://<render>/api/me # Upload (multipart); 'files' must be the field name curl -i -X POST https://<render>/api/files \ -H "Cookie: session=<...>" \ -F "files=@./somefile.pdf"
-
Frontend: Open DevTools → Network tab. Verify requests go to
/api/...andcookieis present in request/response.
- Expiring links / signed URLs
- Drag & drop uploader with progress & cancel
- Shareable short URLs (hashids/shortid)
- Scan analytics per QR (store hits to
/v/:id) - Bulk ZIP download of all QR codes
- Real users DB with password hashing + email verification
MIT (or your choice).