restxlsx
A RESTful web service that exposes Microsoft Excel (.xlsx / .xlsm) tables as a live API. Supports REST CRUD, GraphQL, SQL queries, and DDL operations — all backed by an xlsx file as the data store.
Quick Start
# Use the sample data
./restxlsx
# Or point at your own file
./restxlsx --file data/sample.xlsx
# Open http://localhost:3000/health
Configuration
All settings can be provided via CLI flags or environment variables. Flags take precedence over env vars.
| Flag | Env Var | Default | Description |
|---|---|---|---|
--file |
RESTXLSX_FILE |
data/sample.xlsx |
Path to the xlsx/xlsm data file |
--host |
RESTXLSX_HOST |
localhost |
Interface to bind to (0.0.0.0 for all) |
--port |
RESTXLSX_PORT |
3000 |
TCP port |
--debug |
RESTXLSX_DEBUG |
false |
Enable OpenAPI spec, Swagger UI, GraphiQL |
--quiet |
RESTXLSX_CONSOLE=quiet |
false |
Suppress all stdout output |
--log-reads |
RESTXLSX_LOG_READS |
true |
Log GET/HEAD requests (false to skip) |
--log-file |
RESTXLSX_LOG_FILE |
"" |
Optional path to append logs to file |
--auth |
RESTXLSX_AUTH |
writer |
Fallback auth role (see Authorization below) |
Examples:
# Listen on all interfaces, port 8080, reader-only
./restxlsx --host 0.0.0.0 --port 8080 --auth reader
# Using env vars
RESTXLSX_FILE=./myfile.xlsx RESTXLSX_DEBUG=true ./restxlsx
# Quiet mode, no read logging, log to file
./restxlsx --quiet --log-reads=false --log-file /var/log/restxlsx.log
Endpoints
Health
GET /health
REST CRUD — GET/POST/PUT/DELETE /api/{table}[/{id}]
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/api/tables |
List all discovered table names | reader+ |
GET |
/api/{table} |
List all records in a table | reader+ |
GET |
/api/{table}/{id} |
Get a single record by its first-column ID | reader+ |
POST |
/api/{table} |
Create a new record | writer+ |
PUT |
/api/{table}/{id} |
Update an existing record | writer+ |
DELETE |
/api/{table}/{id} |
Delete a record | writer+ |
The first column of each table is treated as the unique ID column. If omitted on create, an auto-increment value is assigned.
GraphQL — POST /graphql
Query and mutate data via GraphQL. Dynamic schema is generated from the tables at startup.
# Query
{ list_Employees { ID Name Salary } }
# Get single
{ get_Employees(id: "1") { Name Department } }
# Mutate
mutation { create_Employees(ID: "99", Name: "New", Department: "QA", Salary: "70000") { ID } }
When --debug is enabled, GET /graphql serves the GraphiQL IDE.
SQL — POST /api/sql
Execute SQL statements against the data. Supports SELECT, INSERT, UPDATE, DELETE with optional WHERE.
// SELECT
{"query": "SELECT Name, Salary FROM Employees WHERE ID = '1'"}
// SELECT all
{"query": "SELECT * FROM Employees"}
// INSERT
{"query": "INSERT INTO Employees (ID, Name, Department) VALUES ('42', 'Alice', 'Eng')"}
// UPDATE
{"query": "UPDATE Employees SET Salary = '100000' WHERE ID = '42'"}
// DELETE
{"query": "DELETE FROM Employees WHERE ID = '42'"}
The SQL dialect is intentionally minimal — only = comparisons in WHERE, string/number values,
and single statements per request. This maps directly to the in-memory table operations.
DDL — POST/DELETE /api/ddl/*
Schema management (admin only).
| Method | Path | Description |
|---|---|---|
POST |
/api/ddl/sheets |
Create a new sheet (empty, with a default ID column) |
DELETE |
/api/ddl/sheets/{sheet} |
Delete a sheet and all its data |
POST |
/api/ddl/tables |
Define columns on a sheet (effectively creates a table schema) |
DELETE |
/api/ddl/tables/{table} |
Remove all data from a table (keeps the sheet) |
Create sheet:
POST /api/ddl/sheets
{"name": "MyNewSheet"}
Define table on a sheet:
POST /api/ddl/tables
{"sheet": "MyNewSheet", "name": "Projects", "columns": ["ID", "Name", "Budget"]}
OpenAPI / Swagger (debug mode)
When --debug is set:
| Endpoint | Description |
|---|---|
GET /openapi.json |
Auto-generated OpenAPI 3.0.3 specification |
GET /swagger |
Swagger UI interactive documentation |
Authorization
Authorization is implemented as a role-based system with three levels. The --auth flag (or RESTXLSX_AUTH
env var) sets the fallback role that applies to all requests. This is the effective authorization until
OIDC/OAuth2 integration is added (see below).
Role hierarchy
| Role | Read | Write (POST/PUT/DELETE) | DDL (create/drop) |
|---|---|---|---|
reader |
✓ | — | — |
writer (default) |
✓ | ✓ | — |
admin |
✓ | ✓ | ✓ |
For unauthenticated / open access
To run the service with no authentication barrier (everyone can read and write):
./restxlsx --auth writer
This is the default — all callers are treated as writer. They can read and mutate data but
cannot modify the schema. If you also need DDL access:
./restxlsx --auth admin
For read-only public access:
./restxlsx --auth reader
Future: OIDC / OAuth2 integration
The auth architecture (internal/auth/) is designed to be upgraded to token-based claims.
The current --auth fallback role will become the default for unauthenticated requests.
Planned integration path:
- The
auth.Middlewarewill first inspect theAuthorizationheader for a Bearer JWT. - If a valid token is present, the
rolesclaim (or a custom claim mapping) is extracted and the effective role becomes the token's role. - If no token is present (or the token is invalid/expired), the fallback
--authrole applies. - A future
--oidc-issuer,--oidc-client-id, and--oidc-claim-rolesconfig will wire the validation.
Middleware structure (ready for extension):
internal/auth/
├── auth.go # Role type, helpers, RequireWrite/RequireDDL middlewares
├── oidc.go # (future) OIDC token validation, claim extraction
└── middleware.go # (future) Combined middleware: try OIDC, fallback to role
The auth.Middleware currently injects a static role into the request context.
To add OIDC, you would:
- Parse the
Bearertoken in the middleware - Validate against the OIDC issuer's JWKS
- Map the
rolesclaim to aRole - Store it in context (same
roleKey{}) - All downstream
RequireWrite/RequireDDLchecks work unchanged
Data Persistence & Concurrency
The xlsx file on disk is the persistent store. The workflow:
- On startup, the xlsx file is read into memory
- All operations read from the in-memory snapshot (fast, no file I/O on reads)
- Every write operation mutates memory and immediately flushes to the xlsx file
- The file is not held open between flushes — other applications (Excel, etc.) can freely edit the file while the service is running
- During a flush, the service merges any new sheets that were added externally, so external additions are preserved
- If the file cannot be opened for writing (e.g., locked by another process),
the operation fails with
503 Service Unavailable - In-memory data is protected by a
sync.Mutex, so concurrent HTTP requests are serialized at the engine level — safe for concurrent access
Note: Updates to existing rows from an external editor will be overwritten by the next flush from this service. For collaborative editing, keep the xlsx as the primary source and use this service as a proxy.
Docker
# Build
docker build -t restxlsx .
# Run (default sample data included)
docker run -p 3000:3000 restxlsx
# With your own xlsx file
docker run -p 3000:3000 -v /path/to/your.xlsx:/data/sample.xlsx restxlsx
The Dockerfile uses a multi-stage build resulting in a ~15 MB scratch image.
Development
Prerequisites
- Go 1.24+
- Python 3.10+ (only needed for sample data generation)
Regenerate sample data
python3 -m venv /tmp/venv && /tmp/venv/bin/pip install openpyxl
/tmp/venv/bin/python scripts/gen_data.py
Project layout
restxlsx/
├── main.go # Entry point, router, graceful shutdown
├── Dockerfile # Multi-stage scratch build
├── internal/
│ ├── config/config.go # CLI flags + env var configuration
│ ├── logger/logger.go # Structured logging via slog
│ ├── excel/excel.go # XLSX engine: read, write, flush, DDL
│ ├── auth/auth.go # Role-based authorization middleware
│ ├── graphql/graphql.go # Dynamic GraphQL schema & handler
│ └── api/
│ ├── rest.go # REST CRUD handlers
│ ├── sql.go # SQL endpoint + minimal SQL parser
│ ├── ddl.go # DDL handlers (create/drop sheets/tables)
│ ├── openapi.go # Dynamic OpenAPI spec generation
│ ├── middlewares.go # JSON content-type middleware
│ └── swagger-ui.html # Embedded Swagger UI
├── data/sample.xlsx # Sample workbook (3 tables)
├── scripts/gen_data.py # Python script to generate sample data
└── tasks.md # Architecture decisions & progress log
Building from Source
Cross-platform builds
The scripts/build.sh script cross-compiles binaries for Linux, Windows, and macOS:
chmod +x scripts/build.sh
./scripts/build.sh
Binaries are placed in the dist/ directory:
| File | Platform |
|---|---|
dist/restxlsx-linux-amd64 |
Linux (x86_64) |
dist/restxlsx-windows-amd64.exe |
Windows (x86_64) |
dist/restxlsx-darwin-amd64 |
macOS (Intel) |
dist/restxlsx-darwin-arm64 |
macOS (Apple Silicon) |
Manual build
go build -o restxlsx .
Dependency rationale
| Library | Purpose |
|---|---|
github.com/go-chi/chi/v5 |
Lightweight, idiomatic HTTP router |
github.com/xuri/excelize/v2 |
Full xlsx read/write support |
github.com/graphql-go/graphql |
Dynamic schema generation from data |