first commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
npm-debug.log*
|
||||||
16
Dockerfile
Normal file
16
Dockerfile
Normal 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
71
README.md
Normal 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
22
docker-compose.local.yml
Normal 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
45
docker-compose.prod.yml
Normal 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
1133
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
package.json
Normal file
17
package.json
Normal 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
83
server.js
Normal 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}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user