A lightweight, zero-ops JSON micro-database and API server. Store schemaless JSON documents in named sets and collections, query them with filters, optionally validate against JSON Schema, and add JSON-path indexes for speed. Ships with a built-in dashboard and an MCP interface.
- Storage: SQLite with WAL, one physical table per set:
data_<set>
storing{id, collection, data JSON, created_at, updated_at}
. - API: Clean REST endpoints for documents, collections, sets, indexing, and schemas.
- Query: JSON where filters with rich operators ($eq, $ne, $gt, $gte, $lt, $lte, $like, $ilike, $startsWith, $istartsWith, $endsWith, $iendsWith, $contains, $icontains, $in, $nin, $between, $isNull, $notNull), plus order, limit/offset, and pagination header.
- Indexes: Async JSON-path indexes tracked in metadata and usage-counted for observability.
- Validation: Optional per-collection JSON Schema validation on create/update/replace.
- Dashboard: Single-page UI served from
/
for exploring data and testing APIs. - MCP: Two flavors: HTTP endpoints at
/mcp
and a standalone stdio MCP server (cmd/micro-api-mcp
).
# Persist DB to ./data.db and expose on 8080
docker run --rm -p 8080:8080 \
-v "$(pwd)":/data \
-e DB_PATH=/data/data.db \
ghcr.io/fernandezvara/microapi:latest
- Health:
curl http://localhost:8080/health
- Dashboard: open http://localhost:8080/
See docker-compose.yaml
. Start both services:
docker compose up --build
The Compose file mounts a community n8n node from ../n8n-nodes-microapi
for convenience.
make run # builds and runs ./cmd/micro-api
make test # run tests
make css # builds web/static/style.css via Tailwind (requires Node + npx)
Go 1.24+ recommended. Config is read from environment and optional .env
.
Defined in internal/config/config.go
.
- PORT (default
8080
): HTTP port. - DB_PATH (default
./data.db
): SQLite file path. - MAX_REQUEST_SIZE (default
1048576
): Max request body bytes. - ALLOW_DELETE_SETS (default
false
): EnableDELETE /{set}
. - ALLOW_DELETE_COLLECTIONS (default
false
): EnableDELETE /{set}/{collection}
. - CORS (default empty): CSV of allowed Origins. Empty means reflect any Origin.
- DEV (default
false
): Dev mode flag (currently used for minor toggles).
CORS exposes the X-Total-Items
header to browsers.
- Set: Top-level namespace. Backed by table
data_<set>
. - Collection: Logical group inside a set. Stored in the
collection
column. - Document: Arbitrary JSON in
data
column plus generated metadata timestamps.
SQLite is opened with WAL mode, foreign keys ON, busy timeout, and synchronous NORMAL (internal/database/connection.go
). Per-set tables have helpful indexes on collection
and (collection, created_at)
.
Every response uses internal/models.APIResponse
:
{ "success": true, "data": ..., "error": null }
- Errors return
success:false
witherror
message and appropriate HTTP status. - Document responses include
_meta
unless suppressed (see below).
Base path is /
. Names ({set}
, {collection}
) must match ^[a-zA-Z0-9_]+$
.
- GET
/health
→{ status: "ok", version: <string> }
- GET
/
serves the UI. Assets:/style.css
,/favicon.ico
,/logo.svg
.
- GET
/_sets
→ summary of sets with collection/doc counts. - GET
/{set}
→ per-collection stats for the set. - DELETE
/{set}
→ drops the set table and metadata. RequiresALLOW_DELETE_SETS=true
.
Example:
curl http://localhost:8080/_sets
curl http://localhost:8080/myset
- POST
/{set}/{collection}
→ create document. - GET
/{set}/{collection}
→ query documents (see Query section). - GET
/{set}/{collection}/{id}
→ fetch one. - PUT
/{set}/{collection}/{id}
→ replace document (full body). - PATCH
/{set}/{collection}/{id}
→ merge patch. - DELETE
/{set}/{collection}/{id}
→ delete by id. - DELETE
/{set}/{collection}
→ delete all or filtered (requiresALLOW_DELETE_COLLECTIONS=true
).
Metadata in responses:
_meta.id
,_meta.created_at
,_meta.updated_at
are added by default.- Suppress with
?meta=0
on GET/Query endpoints.
Endpoint: GET /{set}/{collection}
with query params:
where
: JSON string. Example:{"user.name": {"$eq": "Alice"}}
order_by
:created_at
,updated_at
, or a JSON path like$.user.age
limit
: integer > 0offset
: integer ≥ 0debug=1
: addsX-Query-Plan
header withEXPLAIN QUERY PLAN
summary
Pagination:
- Response header
X-Total-Items
includes the total count ignoring limit/offset.
Supported operators in where
:
- Comparisons:
$eq
,$ne
,$gt
,$gte
,$lt
,$lte
- Text pattern:
$like
(SQL LIKE pattern e.g. "%foo%")$ilike
(case-insensitive LIKE)$startsWith
,$istartsWith
$endsWith
,$iendsWith
$contains
,$icontains
(wraps value with%...%
)
- Sets:
$in
,$nin
(expects an array of values) - Range:
$between
(expects[min, max]
) - Null checks:
$isNull
,$notNull
(value ignored)
Notes:
$in
with an empty array matches no rows;$nin
with an empty array matches all rows.- Case-insensitive operators use
LOWER(...)
under the hood. - In
where
, you can use dot paths likeuser.age
or JSONPath (e.g.$.user.age
). - For
order_by
and index endpoints, JSONPath is accepted (e.g.$.user.age
).
Paths:
where
keys can use dot notation (user.age
) or JSONPath ($.user.age
).order_by
and index endpoints accept JSONPath (e.g.$.user.age
).
- Malformed
where
returns HTTP 400 with a friendly message:"malformed where clause: expected a JSON object where keys are field paths and values are operator objects"
- Unsupported operator returns HTTP 400, e.g.
"unsupported operator: $foo"
. - Empty results always return an empty array
[]
(nevernull
). - Multi-argument expectations:
$in
/$nin
require an array value.$between
requires a two-element array[min, max]
.
Examples of bad requests:
# Malformed JSON
curl "http://localhost:8080/myset/users?where=not-json"
# Unsupported operator
curl "http://localhost:8080/myset/users?where={\"age\":{\"$foo\":1}}"
# Wrong shape for $between (should be [min,max])
curl "http://localhost:8080/myset/users?where={\"age\":{\"$between\":42}}"
Examples:
# All docs where user.age >= 18, newest first, first page of 10
curl "http://localhost:8080/myset/users?where={\"user.age\":{\"$gte\":18}}&order_by=created_at&limit=10&offset=0"
# Suppress metadata
curl "http://localhost:8080/myset/users?where={}&meta=0"
# Text pattern matching
curl "http://localhost:8080/myset/users?where={\"user.name\":{\"$contains\":\"Ada\"}}"
curl "http://localhost:8080/myset/users?where={\"user.name\":{\"$icontains\":\"ada\"}}" # case-insensitive
curl "http://localhost:8080/myset/users?where={\"user.name\":{\"$startsWith\":\"An\"}}"
curl "http://localhost:8080/myset/users?where={\"user.name\":{\"$istartsWith\":\"an\"}}" # case-insensitive
curl "http://localhost:8080/myset/users?where={\"user.email\":{\"$endsWith\":\"@example.com\"}}"
curl "http://localhost:8080/myset/users?where={\"user.email\":{\"$iendsWith\":\"@EXAMPLE.COM\"}}" # case-insensitive
# Set membership
curl "http://localhost:8080/myset/orders?where={\"status\":{\"$in\":[\"new\",\"processing\"]}}"
curl "http://localhost:8080/myset/orders?where={\"status\":{\"$nin\":[\"cancelled\"]}}"
# Range
curl "http://localhost:8080/myset/products?where={\"price\":{\"$between\":[10,20]}}"
# Null checks
curl "http://localhost:8080/myset/users?where={\"archivedAt\":{\"$isNull\":true}}"
curl "http://localhost:8080/myset/users?where={\"archivedAt\":{\"$notNull\":true}}"
# Order by JSON path
curl "http://localhost:8080/myset/users?order_by=$.user.age"
# Pagination and debug header
curl -i "http://localhost:8080/myset/users?limit=5&offset=5&debug=1"
Create, inspect, and remove JSON-path indexes per collection. Index creation is asynchronous.
- POST
/{set}/{collection}/_index
- Body:
{ "path": "$.user.age" }
or{ "paths": ["$.user.age", "$.country"] }
- Response:
202 Accepted
,{ name, status: "creating" }
- Body:
- GET
/{set}/{collection}/_indexes
→ list index metadata. - GET
/{set}/{collection}/_index/{path}
→ status for an index.path
is URL-encoded JSONPath. Example for$.user.age
:%24.user.age
- DELETE
/{set}/{collection}/_index/{path}
orDELETE .../_index/{path}?paths=p1,p2
→ drops index and metadata.
Index metadata fields (internal/database/index.go
):
paths
(CSV),status
(creating|ready|error
),error
,usage_count
,last_used_at
,created_at
.
- PUT
/{set}/{collection}/_schema
- Body: JSON Schema to enable validation, or
null
/empty to remove schema. - Response:
{ schema: <echoed-or-null> }
- Body: JSON Schema to enable validation, or
- GET
/{set}/{collection}/_info
- Response:
{ schema, indexes, stats: { count, created_at? } }
- Response:
Documents are validated on create/replace/update when a schema is set.
- Name validation: set/collection must match
^[a-zA-Z0-9_]+$
. - Reserved fields: top-level keys starting with
_
are reserved in documents._meta
in request bodies is ignored/validated and never stored. - Body limit:
MAX_REQUEST_SIZE
enforced via middleware. - CORS: Allow list via
CORS
env var;X-Total-Items
is exposed for pagination.
- Multi-stage
Dockerfile
builds a static binary and exposes port 8080. - Published images:
ghcr.io/fernandezvara/microapi:<tag>
with multi-arch manifest (latest
tag points to the newest release). Makefile
targets:build
,run
,test
,css
.
# Create a document
curl -X POST http://localhost:8080/myset/users \
-H 'Content-Type: application/json' \
-d '{"user":{"name":"Ada","age":37}}'
# Get it back (replace <id>)
curl http://localhost:8080/myset/users/<id>
# Query (age >= 30), order by JSON path
curl "http://localhost:8080/myset/users?where={\"user.age\":{\"$gte\":30}}&order_by=$.user.age"
# Create an index
curl -X POST http://localhost:8080/myset/users/_index \
-H 'Content-Type: application/json' \
-d '{"path":"$.user.age"}'
# Set a JSON Schema
curl -X PUT http://localhost:8080/myset/users/_schema \
-H 'Content-Type: application/json' \
-d '{"type":"object","properties":{"user":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"}}}},"required":["user"]}'
cmd/micro-api/
: HTTP server main.cmd/micro-api-mcp/
: MCP stdio server main.internal/server/server.go
: router and middleware wiring.internal/handlers/
: REST and MCP handlers.internal/query/
: where parser and SQL builder.internal/database/
: connection, migrations, indexes, per-set table helpers.internal/validation/
: JSON Schema persistence and validation.web/static/
: dashboard (dashboard.html
,style.css
).docker-compose.yaml
: local stack with optional n8n.
n8n integration is provided via the n8n-nodes-microapi
community node. repo: https://github.com/fernandezvara/n8n-nodes-microapi
MIT. See LICENSE
.