# ROAPI: read-only SQL, GraphQL and REST APIs for static datasets

> ROAPI turns CSV, Parquet, JSON and database tables into query endpoints without application code. The trade-off is that it is a query server for slowly moving data, not an operational database.

**roapi/roapi** — Create full-fledged APIs for slowly moving datasets without writing a single line of code.

- Repository: https://github.com/roapi/roapi
- Website: https://roapi.github.io/docs
- Stars: 3,434 · Forks: 209
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/roapi-roapi

## What ROAPI is for, and who it is not for

The README states the goal plainly: ROAPI "automatically spins up read-only APIs for static datasets without requiring you to write a single line of code." That sentence contains the whole scope. The datasets are static or slowly moving. The APIs are read-only. The value is that you skip writing a service layer, not that you skip thinking about the data.

The intended user is someone who already has files or tables and needs them queryable over HTTP. The README's own quick start points at `test_data/uk_cities_with_headers.csv` and `test_data/spacex_launches.json`, which tells you the project expects local files as a first-class input. The config example goes further, listing a Parquet file, a JSON file with a JSON pointer and an explicit column schema, a live SpaceX API endpoint, and an archived GitHub jobs JSON URL. So the realistic use case is a small set of heterogeneous sources that a team wants behind one query surface.

It is the wrong tool when the data changes under you. There is no write path in the documented API. If your application needs inserts, updates or transactions, ROAPI is not a database and the README does not present it as one. It is also a poor fit when you need per-user authorization: the documented query frontends are open endpoints on the address you configure, and the README says nothing about authentication or row-level policies.

## The four-layer design: frontends, Datafusion, data loading, encoding

ROAPI's architecture is stated as four parts, and the split explains most of its behaviour.

The query frontends translate SQL, FlightSQL, GraphQL and REST queries into Datafusion plans. Datafusion then executes those plans. A data layer loads datasets from various sources and formats with automatic schema inference. Finally, a response encoding layer serializes intermediate Arrow record batches into whatever format the client asked for.

Two consequences follow from that design. First, the query languages are not independent implementations. The README says the GraphQL frontend "supports the same set of operators supported by REST query frontend", and the REST frontend supports exactly four: columns, sort, limit, filter. So GraphQL here is not a general graph API; it is a typed wrapper over the same filter, sort and limit primitives. Second, because Datafusion executes the plan, the SQL frontend is the most expressive of the four, and the README calls it "the only query interface that" supports something more (the sentence is truncated in the README excerpt).

The encoding layer is the part most people miss. Responses default to JSON, but the client can request a different encoding with the `ACCEPT` header, including `application/vnd.apache.arrow.stream`. That means the same endpoint can serve a browser and an Arrow-native consumer without a second service.

## Installing ROAPI and serving your first table

The README offers three install paths: Homebrew, pip, and a source build. Pre-built binaries are on the GitHub release page, and pre-built Docker images are published at `ghcr.io/roapi/roapi`.

```bash
brew install roapi
# or
pip install roapi
```

If you prefer to build from source, the README gives this command, which installs the binaries from the main branch with a locked dependency set:

```bash
cargo install --locked --git https://github.com/roapi/roapi --branch main --bins roapi
```

The fastest way to see it work is the quick start, which registers two local files as tables. The syntax is `--table "name=path"`, and when you omit the name, ROAPI derives it from the file:

```bash
roapi \
    --table "uk_cities=test_data/uk_cities_with_headers.csv" \
    --table "test_data/spacex_launches.json"
```

Once it is running, the README points at a builtin web UI at `http://localhost:8080/ui`, and the same port serves the query endpoints. You can check what ROAPI inferred about your files with the schema endpoint, which the README documents as returning the inferred schema for all tables:

```bash
curl 'localhost:8080/api/schema'
```

Then query. The README gives three equivalent requests against the default JSON encoding, one per frontend:

```bash
curl -X POST -d "SELECT city, lat, lng FROM uk_cities LIMIT 2" localhost:8080/api/sql
curl -X POST -d "query { uk_cities(limit: 2) {city, lat, lng} }" localhost:8080/api/graphql
curl "localhost:8080/api/tables/uk_cities?columns=city,lat,lng&limit=2"
```

If you would rather not install anything, the Docker invocation maps port 8080 and passes the same table arguments, with `--addr-http 0.0.0.0:8080` so the server listens on all interfaces inside the container:

```bash
docker run -t --rm -p 8080:8080 ghcr.io/roapi/roapi:latest --addr-http 0.0.0.0:8080 \
    --table "uk_cities=test_data/uk_cities_with_headers.csv" \
    --table "test_data/spacex_launches.json"
```

## Config files, MySQL and SQLite sources, and dynamic table registration

Command-line flags stop being enough once a table needs format-specific options. The README directs you to a YAML or TOML config file, which "supports more advanced format specific table options". The example config sets the HTTP address and a Postgres address, then declares four tables: a Parquet file, a JSON file with a pointer and an explicit eight-column schema, a live URL, and an archived URL.

```yaml
addr:
  http: 0.0.0.0:8080
tables:
  - name: "blogs"
    uri: "test_data/blogs.parquet"
  - name: "ubuntu_ami"
    uri: "test_data/ubuntu-ami.json"
    option:
      format: "json"
      pointer: "/aaData"
      array_encoded: true
```

Run it with `roapi -c ./roapi.yml` (the README notes `.toml` also works). The `schema.columns` block in the full example is worth noting: when a JSON source has fields you want typed as `Utf8` rather than inferred, you declare them, which is the escape hatch for messy inputs.

Databases are registered through the same `--table` flag, using a connection URI as the value. The README shows both forms:

```bash
--table "table_name=mysql://username:password@localhost:3306/database"
--table "table_name=sqlite://path/to/database"
```

There is also a dynamic path. Adding `-d` to the command enables runtime registration, and the README is explicit that `--table` cannot be omitted even then. With `-d` set, you POST a JSON array to `/api/table` with `tableName` and `uri` keys to register additional sources.

```bash
roapi \
    --table "uk_cities=test_data/uk_cities_with_headers.csv" \
    -d
```

That flag combination is the part of ROAPI that comes closest to a live service, and it is also the part with the thinnest documentation. The README does not describe what happens to a table that is re-registered under an existing name, nor how to remove one.

## Where ROAPI's query surface runs out

The REST frontend supports four operators, and that list is a hard boundary rather than a starting point. Sorting is expressed as `sort=col1,-col2`, where the minus sign means descending. Filtering uses bracket syntax with comparison suffixes: `filter[col1]='foo'` for equality, and `filter[col2]gte=0&filter[col2]lt=5` for a range. There is no documented aggregation, no join, no group-by, and no projection beyond choosing columns. If you need any of those, you are meant to use the SQL endpoint instead.

The GraphQL frontend inherits that ceiling. Its filter object nests operators (`filter: { col1: false, col2: { gteq: 4, lt: 1000 } }`) and its sort takes a list of field and order pairs, but the README states it supports the same operator set as REST. Anyone expecting GraphQL to bring relationships or nested resolvers will be disappointed; the schema is generated from tables, not from a graph you author.

There is also a platform papercut worth knowing before you script anything. The README notes that on Windows the full scheme (`file://` or `filesystem://`) must be filled in, and that you should use double quotes instead of single quotes to escape the command-line length limit. That is a real difference in invocation, not a cosmetic one, and it means Windows examples copied from Unix documentation will not run unchanged.

## How ROAPI compares to columnq and to a hand-written service

The repository is a Cargo workspace with four members: `columnq`, `columnq-cli`, `roapi-ui`, and `roapi`. That layout is the most useful comparison available, because columnq ships in the same tree and reuses the same query engine.

The difference is the surface. columnq is a query engine and CLI: you point it at files and run queries locally. ROAPI wraps that engine in HTTP frontends (SQL, FlightSQL, GraphQL, REST) plus a response encoding layer and a browser UI built with trunk as a WebAssembly target, which the Dockerfile builds separately and embeds into the release binary. So the choice is not about query capability but about whether you need a network endpoint and multiple client protocols. If your job is a one-off analysis, the CLI is the smaller thing to install; if three teams need to query the same Parquet file from different languages, the server is the point.

The other alternative is writing the service yourself. A small HTTP handler over Datafusion or DuckDB would give you exactly the filters and auth model you want, and it would not impose ROAPI's four-operator REST vocabulary. What you would give up is the config file, the automatic schema inference across CSV, Parquet, JSON, MySQL and SQLite, the Arrow stream encoding, and the builtin UI. For a single table and a single consumer, that trade is probably not worth it. For a catalog of heterogeneous sources, it is.

## Maintenance, licensing, and the cost of upgrading

The repository is not archived, and the last push was on 2026-03-25. The most recent release, `roapi-v0.13.0`, is dated 2026-03-24, one day earlier. Before that, `roapi-v0.12.7` shipped on 2026-03-22 and `roapi-v0.12.6` on 2025-06-16. That sequence is worth reading carefully: two releases landed two days apart in March 2026, and the gap before them was roughly nine months. The project is not dormant, but it does not ship on a predictable cadence, so pinning a version rather than tracking `main` is the safer default.

Upgrade cost is dominated by the dependency graph rather than by ROAPI's own API. The workspace pins `Cargo.lock`, and the Dockerfile builds with `--locked`, which means the binary you get from a container build matches the committed lockfile. The Dockerfile also accepts a `FEATURES` build argument defaulting to `database,ui`, so a build without the UI or without database connectors is possible if you want a smaller binary. The source install command in the README uses `--locked` as well.

Licensing is Apache-2.0, per the repository. That permits commercial use and modification, and it includes a patent grant. It also means that if you modify and redistribute ROAPI, the Apache-2.0 notice and attribution requirements apply. This is a description of the licence identifier, not legal advice; check the full text in `LICENSE.txt` and your own obligations before shipping a modified binary.

## Conclusion

Adopt ROAPI when you have datasets that change on a schedule, not per request, and you want SQL, GraphQL and REST endpoints without writing a service. Skip it when you need writes, transactions or row-level access control, because the README describes read-only APIs and the REST frontend exposes only columns, sort, limit and filter. Before committing, verify two things against your own files: that schema inference produces the column types you expect, and that your chosen encoding (JSON by default, or Arrow via the ACCEPT header) is what your client can consume.

## FAQ

### What is ROAPI and what problem does it solve?

ROAPI automatically creates read-only APIs for static datasets without you writing code. It builds on Apache Arrow and Datafusion, translating SQL, FlightSQL, GraphQL and REST queries into Datafusion plans over data loaded from files, URLs or databases.

### How do I install ROAPI?

The README lists three options: Homebrew with brew install roapi, pip with pip install roapi, or a source build with cargo install. Pre-built binaries are on the GitHub release page and pre-built images are published at ghcr.io/roapi/roapi.

### Can ROAPI read Parquet, CSV and JSON files?

Yes. The quick start registers a CSV file and a JSON file as tables, and the config file example includes a Parquet file, a JSON file with a pointer and explicit schema, plus a remote JSON URL. The README also shows MySQL and SQLite registered through the --table flag.

### Does ROAPI support GraphQL and REST as well as SQL?

It does. SQL goes to /api/sql, GraphQL to /api/graphql, and REST queries to /api/tables/{table_name}. The README states the GraphQL frontend supports the same operator set as the REST frontend, which covers columns, sort, limit and filter.

### How do I get a non-JSON response from ROAPI?

Responses are JSON by default, and you request another encoding with the ACCEPT header. The README shows application/vnd.apache.arrow.stream for an Arrow stream response.

## Sources

- [License: Apache-2.0](https://github.com/roapi/roapi/blob/main/LICENSE)
- [Project website](https://roapi.github.io/docs)
- [README](https://github.com/roapi/roapi/blob/main/README.md)
- [Releases](https://github.com/roapi/roapi/releases)
- [roapi/roapi on GitHub](https://github.com/roapi/roapi)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/roapi-roapi
