Serverless Excel export microservice for the Tapiz academic management platform. Handles score sheet workbooks and form response exports.
Built with Hono + TypeScript on Node.js, deployed as a Vercel serverless function.
| HTTP framework | Hono |
| Excel engine | ExcelJS |
| Rate limiting | hono-rate-limiter (Valkey-backed) |
| Cache / ephemeral | Valkey (Redis-compatible, via ioredis) |
| Runtime | Node.js ≥ 18 |
| Deployment | Vercel Serverless |
src/
├── app.ts # Hono app — route mounting, middleware, docs
├── index.ts # Local dev entry point
├── core/
│ ├── colors.ts # Warm brown RGB palette (matches frontend CSS vars)
│ ├── docs.ts # OpenAPI-style HTML docs page
│ ├── Result.ts # Ok / Err result type
│ └── valkeyClient.ts # ioredis singleton
├── middleware/
│ ├── errorHandler.ts # Global Hono error handler
│ └── rateLimiter.ts # 10 req / 15 min per IP, Valkey-backed
└── routes/
├── scoresheetRoute.ts # POST /api/excel/score-sheet
└── formsRoute.ts # POST /api/excel/forms
api/
└── index.ts # Vercel serverless handler (Node → Web Request bridge)
- Node.js ≥ 18
- A running Valkey / Redis instance (optional — rate limiter falls back to in-memory if unavailable)
npm installCopy the example and fill in your values:
cp .env.example .env| Variable | Required | Description |
|---|---|---|
PORT |
Local dev port (default: 3003) |
|
CLIENT_URL |
CORS allowed origin(s), comma-separated (default: *) |
|
VALKEY_URL |
Redis-compatible URL, e.g. redis://localhost:6379 or rediss://... for TLS |
npm run devServer starts at http://localhost:3003.
Interactive API docs are available at http://localhost:3003/.
npm run typecheckThis project is designed for Vercel serverless deployment — no build step is needed locally. For a standalone Node.js build, tsc output goes to dist/.
All endpoints are prefixed with /api/excel. A health check is available at:
GET /api/excel/health → { "status": "ok", "ts": "..." }
Interactive docs with full request/response schemas are served at the root URL:
GET / → HTML docs page
GET /api/docs → HTML docs page
| Method | Path | Description | Output |
|---|---|---|---|
POST |
/api/excel/score-sheet |
Score sheet workbook with stats and dashboard | .xlsx |
POST |
/api/excel/forms |
Form responses export with frequency summary | .xlsx |
All endpoints:
- Accept
Content-Type: application/jsonwith a 50 MB body limit - Return
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheetwithContent-Disposition: attachment - Return
429when the rate limit is exceeded
10 requests per IP per 15 minutes, applied across all /api/excel/* routes.
Backed by Valkey for consistency across serverless instances. Degrades gracefully to in-memory if Valkey is unavailable.
Generates a .xlsx workbook with three sheets:
- Tabela poena — main data table with a max-points sub-header row and per-column average row. Numeric cells are color-coded green / amber / red by percentage of
maxPoints.formulacolumns are rendered bold in primary color. - Statistike — per-column statistics block: count, sum, average, median, standard deviation, min, max, average percentage, and pass rate (≥ 60%).
- Pregled — KPI summary (total students, overall average %, pass rate, number of graded activities) and a per-student points breakdown table with a Položio / Uslovno / Pao status column.
{
"sheetName": "Matematika 1",
"subjectName": "Matematika 1",
"academicYear": "2024/25",
"columns": [
{ "id": "col1", "name": "Kolokvijum 1", "maxPoints": 30 },
{ "id": "col2", "name": "Kolokvijum 2", "maxPoints": 30 },
{ "id": "total", "name": "Ukupno", "maxPoints": 60, "type": "formula" }
],
"rows": [
{
"studentName": "Ana Petrović",
"indexNumber": "1234/24",
"computedCells": { "col1": 24, "col2": 27, "total": 51 }
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
sheetName |
string | Main worksheet tab name (default: "Tabela poena") |
|
subjectName |
string | Subject name shown in the title row | |
academicYear |
string | Academic year label, e.g. "2024/25" |
|
columns |
Column[] |
✓ | Column definitions |
rows |
Row[] |
✓ | Student data rows |
Column object
| Field | Type | Description |
|---|---|---|
id |
string | Unique column identifier |
name |
string | Column header label |
maxPoints |
number? | Maximum achievable points — used for color coding and pass rate |
type |
string? | "formula" renders the cell bold in primary color |
isHidden |
boolean? | If true, column is excluded from the export |
Row object
| Field | Type | Description |
|---|---|---|
studentName |
string | Full student name |
indexNumber |
string | Student index / ID number |
computedCells |
Record<string, unknown> |
Map of columnId → cell value |
Generates a .xlsx workbook with two sheets:
- Odgovori — one row per response, frozen header, alternating row shading.
- Pregled — metadata summary (total responses, number of questions, generation timestamp) followed by a frequency table for every question with
type: "select","radio", or"checkbox". Options are sorted by frequency descending.
{
"formTitle": "Anketa o zadovoljstvu",
"questions": [
{ "id": "q1", "label": "Ime i prezime" },
{ "id": "q2", "label": "Ocena kursa", "type": "select" },
{ "id": "q3", "label": "Komentar" }
],
"responses": [
{
"submittedAt": "2025-06-01T10:30:00.000Z",
"answers": { "q1": "Ana Petrović", "q2": "5", "q3": "Odlično!" }
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
formTitle |
string | Form title shown in the workbook header | |
questions |
Question[] |
✓ | Question definitions |
responses |
Response[] |
✓ | Submitted response records |
Question object
| Field | Type | Description |
|---|---|---|
id |
string | Unique question identifier |
label |
string | Column header in the responses sheet |
type |
string? | "select" / "radio" / "checkbox" — triggers a frequency table in the summary sheet |
Response object
| Field | Type | Description |
|---|---|---|
submittedAt |
string | ISO 8601 submission timestamp |
answers |
Record<string, unknown> |
Map of questionId → value. Array values (multi-select) are joined with , |
All workbooks share a consistent visual language that matches the Tapiz frontend:
- Primary color: Warm brown
#a08040(matches--color-primary-500CSS variable) - Title rows: Primary background with white bold text
- Header rows:
gray-50background, medium bottom border in primary color, frozen pane - Data rows: Alternating white /
gray-50row shading - Numeric cells: Color-coded green / amber / red relative to
maxPoints - Formula cells: Bold text in primary color
- Statistics sheets: Per-column blocks with labeled key-value rows and bordered cells
The project is configured for Vercel via vercel.json. The api/index.ts file acts as the serverless handler — it bridges Node.js IncomingMessage / ServerResponse to the Hono Web Request API, matching the same pattern used across Tapiz microservices.
vercel deployNo additional configuration is needed beyond setting the environment variables in the Vercel dashboard.