first commit

This commit is contained in:
Frank John Begornia
2026-04-02 14:58:00 +08:00
commit c38837b552
9 changed files with 1394 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
npm-debug.log
.git
.gitignore

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
.env
npm-debug.log*

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM node:20-alpine
WORKDIR /app
# Install production dependencies first for better build cache reuse.
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

71
README.md Normal file
View File

@@ -0,0 +1,71 @@
# QR Code API (Node.js)
Simple micro app that returns a QR code image from an API call.
## Setup
```bash
npm install
```
## Run
```bash
npm run dev
# or
npm start
```
Default URL: `http://localhost:3000`
## Endpoints
- `GET /health`
- `GET /api/qr?text=Hello%20World`
- `POST /api/qr` with JSON body
### GET example
```bash
curl "http://localhost:3000/api/qr?text=Hello%20World&size=400" --output qr.png
```
### POST example
```bash
curl -X POST "http://localhost:3000/api/qr" \
-H "Content-Type: application/json" \
-d '{"text":"https://crewsportswear.app","size":500,"margin":2}' \
--output qr.png
```
## Optional parameters
- `size` (number, default `512`, min `128`, max `2048`)
- `margin` (number, default `2`, min `0`, max `10`)
- `dark` (hex color, default `#000000`)
- `light` (hex color, default `#FFFFFF`)
## Docker
### Local Docker run
```bash
docker compose -f docker-compose.local.yml up -d --build
```
Local URL: `http://localhost:3000`
### Production (Traefik)
```bash
docker compose -f docker-compose.prod.yml up -d --build
```
Traefik host rule: `qr.crewsportswear.app`
Notes:
- Uses external Docker networks: `traefik-public` and `crew-app-net`
- Internal service port is `3000`
- Includes HTTP -> HTTPS redirect via Traefik labels

22
docker-compose.local.yml Normal file
View File

@@ -0,0 +1,22 @@
services:
qr-code-api:
build:
context: .
dockerfile: Dockerfile
container_name: qr_code_api_local
restart: unless-stopped
environment:
- NODE_ENV=development
- PORT=3000
ports:
- "3000:3000"
command: npm run dev
volumes:
- ./:/app
- /app/node_modules
networks:
- qr-local
networks:
qr-local:
driver: bridge

45
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,45 @@
services:
qr-code-api:
image: qr-code-api:latest
build:
context: .
dockerfile: Dockerfile
container_name: qr_code_api
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
labels:
- "traefik.enable=true"
# HTTPS router
- "traefik.http.routers.qr-code-api.rule=Host(`qr.crewsportswear.app`)"
- "traefik.http.routers.qr-code-api.entrypoints=websecure"
- "traefik.http.routers.qr-code-api.tls=true"
- "traefik.http.routers.qr-code-api.tls.certresolver=le"
# Service
- "traefik.http.services.qr-code-api.loadbalancer.server.port=3000"
# HTTP to HTTPS redirect
- "traefik.http.routers.qr-code-api-http.rule=Host(`qr.crewsportswear.app`)"
- "traefik.http.routers.qr-code-api-http.entrypoints=web"
- "traefik.http.routers.qr-code-api-http.middlewares=https-redirect"
- "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
networks:
- traefik-public
- crew-app-net
- default
networks:
traefik-public:
external: true
crew-app-net:
external: true

1133
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "qr-code-api",
"version": "1.0.0",
"description": "Simple Node.js micro API that returns QR code PNG images",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.2.1",
"qrcode": "^1.5.4"
}
}

83
server.js Normal file
View File

@@ -0,0 +1,83 @@
const express = require("express");
const QRCode = require("qrcode");
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json({ limit: "100kb" }));
function parseIntWithBounds(value, fallback, min, max) {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
return fallback;
}
return Math.min(max, Math.max(min, parsed));
}
function normalizePayload(source) {
const payload = source || {};
const text = typeof payload.text === "string" ? payload.text.trim() : "";
if (!text) {
return {
error: "Missing required parameter: text",
};
}
return {
text,
width: parseIntWithBounds(payload.size, 512, 128, 2048),
margin: parseIntWithBounds(payload.margin, 2, 0, 10),
dark: typeof payload.dark === "string" && payload.dark ? payload.dark : "#000000",
light: typeof payload.light === "string" && payload.light ? payload.light : "#FFFFFF",
};
}
async function sendQrPng(source, res, next) {
const normalized = normalizePayload(source);
if (normalized.error) {
return res.status(400).json({ error: normalized.error });
}
try {
const image = await QRCode.toBuffer(normalized.text, {
type: "png",
width: normalized.width,
margin: normalized.margin,
color: {
dark: normalized.dark,
light: normalized.light,
},
});
res.setHeader("Content-Type", "image/png");
res.setHeader("Cache-Control", "no-store");
return res.send(image);
} catch (error) {
return next(error);
}
}
app.get("/", (_req, res) => {
res.json({
service: "qr-code-api",
usage: "GET /api/qr?text=hello or POST /api/qr with JSON body { text }",
});
});
app.get("/health", (_req, res) => {
res.json({ status: "ok" });
});
app.get("/api/qr", (req, res, next) => sendQrPng(req.query, res, next));
app.post("/api/qr", (req, res, next) => sendQrPng(req.body, res, next));
app.use((err, _req, res, _next) => {
console.error(err);
res.status(500).json({ error: "Failed to generate QR code" });
});
app.listen(PORT, () => {
console.log(`QR API listening on port ${PORT}`);
});