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