FlatAPI turns data files in your GitHub repo into a complete static API. Drop your JSON, YAML, CSV, or Markdown files into a directory, add a config file, and FlatAPI generates paginated endpoints, filtered views, a full-text search index, aggregate statistics, and a SQLite database — all as static files you can host on GitHub Pages or anywhere else.
No server. No database to manage. No accounts to create. Just push your data and get an API.
For each collection of data you define, FlatAPI generates:
| Output | What it is |
|---|---|
/api/{collection}/page/1.json |
Paginated listings of all records |
/api/{collection}/record/{id}.json |
Individual records by ID |
/api/{collection}/by-{field}/{value}.json |
Filtered views by any field you configure |
/api/{collection}/sort-by/{field}-{dir}/page/1.json |
Alternative sort orders |
/api/{collection}/search-index.json |
A lunr.js search index for client-side full-text search |
/api/{collection}/stats.json |
Aggregate statistics: counts by field, min/max/avg for numeric fields |
/api/{collection}/changelog.json |
Git-based change history: who changed what, when |
/api/{collection}/meta.json |
Collection metadata: total count, available filters, field list |
/api/db.sqlite |
A SQLite database containing all collections, queryable in the browser with sql.js |
/api/index.json |
An API directory listing all collections and endpoints |
npm install -g flatapi
Run flatapi init to generate a starter config, or create flatapi.config.yml by hand:
output: api
per_page: 20
collections:
posts:
source: data/posts/
id_field: slug
sort:
field: date
direction: desc
sort_variants:
- field: title
direction: asc
filters:
- field: category
- field: tags
array: true
search:
fields: [title, body, tags]
relations:
- field: author
collection: authors
embed: true
authors:
source: data/authors/
id_field: username
search:
fields: [name, bio]Put your data files in the directories you specified. FlatAPI supports:
- JSON — single objects or arrays
- YAML — single objects or arrays
- CSV — each row becomes a record
- Markdown — frontmatter becomes record fields, body becomes
content
A directory of files (one record per file) or a single file containing an array both work.
flatapi build
This creates an api/ directory with all your endpoints. Serve it with any static file server, or deploy to GitHub Pages.
FlatAPI includes a dev server that watches your data files and rebuilds automatically:
flatapi dev
This starts a local server at http://localhost:4567/ with CORS headers enabled, so you can fetch from it during frontend development. The API rebuilds whenever you add, edit, or remove a data file.
flatapi dev --port 3000 # custom port
flatapi dev --config my-config.yml # custom config
Add this workflow to .github/workflows/deploy-api.yml:
name: Deploy API
on:
push:
branches: [main]
permissions:
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50 # needed for changelog generation
- uses: dvelton/flatapi@v1
- uses: actions/upload-pages-artifact@v3
with:
path: api
- id: deployment
uses: actions/deploy-pages@v4Every push to main rebuilds and deploys your API.
If you already have a site (Jekyll, Hugo, Next.js, etc.), just output the API into your site's build directory:
- uses: dvelton/flatapi@v1
with:
output: _site/api # or public/api, out/api, etc.| Option | Default | Description |
|---|---|---|
output |
api |
Directory where generated files are written |
per_page |
20 |
Default number of records per page |
| Option | Required | Default | Description |
|---|---|---|---|
source |
Yes | -- | Path to a directory of data files or a single file |
id_field |
No | id |
Which field to use as the unique record identifier |
per_page |
No | (global) | Override the global pagination size for this collection |
sort.field |
No | id |
Field to sort records by |
sort.direction |
No | asc |
Sort direction: asc or desc |
sort_variants |
No | [] |
Additional sort orders to generate (list of {field, direction}) |
filters |
No | [] |
List of fields to generate filtered views for |
filters[].field |
Yes | -- | The field name to filter on |
filters[].array |
No | false |
Set to true if the field contains an array (e.g., tags) |
search.fields |
No | -- | Fields to include in the full-text search index |
schema |
No | -- | Path to a JSON Schema file for validation |
relations |
No | [] |
List of fields that reference other collections |
relations[].field |
Yes | -- | The field containing the reference (e.g., author) |
relations[].collection |
Yes | -- | Target collection name (e.g., authors) |
relations[].embed |
No | false |
If true, embed the full related record inline. If false, add a URL link. |
Collections can reference each other. When a post has author: dvelton and you configure a relation, FlatAPI resolves the reference at build time.
With embed: true, the author field in the generated output becomes the full author object:
{
"title": "My Post",
"author": {
"username": "dvelton",
"name": "D. Velton",
"bio": "..."
},
"author_ref": "dvelton"
}With embed: false (default), a URL to the related record is added instead:
{
"title": "My Post",
"author": "dvelton",
"author_url": "authors/record/dvelton.json"
}const res = await fetch('https://you.github.io/api/posts/page/1.json');
const { data, meta } = await res.json();
// data: array of records
// meta: { page, per_page, total_pages, total_records }const res = await fetch('https://you.github.io/api/posts/by-category/tutorial.json');
const { data, meta } = await res.json();const res = await fetch('https://you.github.io/api/posts/sort-by/title-asc/page/1.json');
const { data, meta } = await res.json();import lunr from 'lunr';
const res = await fetch('https://you.github.io/api/posts/search-index.json');
const { index, lookup, fields } = await res.json();
const idx = lunr.Index.load(index);
const results = idx.search('getting started');
for (const result of results) {
console.log(lookup[result.ref]); // { title, body, tags }
}const res = await fetch('https://you.github.io/api/posts/stats.json');
const stats = await res.json();
// stats.total_records -> 142
// stats.filter_counts.category -> { "tutorial": 45, "guide": 32, ... }
// stats.fields.date -> { type: "string", min: "2024-01-15", max: "2026-03-28" }const res = await fetch('https://you.github.io/api/posts/changelog.json');
const { entries } = await res.json();
// entries[0] -> { date: "2026-03-28T...", author: "dvelton", message: "Add new post", files_changed: [...] }For complex queries, use the generated SQLite database with sql.js-httpvfs:
import { createDbWorker } from 'sql.js-httpvfs';
const worker = await createDbWorker([{
from: 'inline',
config: { serverMode: 'full', url: 'https://you.github.io/api/db.sqlite', requestChunkSize: 4096 }
}]);
const results = await worker.db.exec(
"SELECT title, category FROM posts WHERE tags LIKE '%github%' ORDER BY date DESC"
);Generate type definitions from your data:
flatapi types
This creates a flatapi.d.ts file with typed interfaces for each collection, filter value unions, and generic PaginatedResponse<T> types. The types are inferred from your actual data, or from JSON Schemas if you provide them.
import type { Posts, PaginatedResponse } from './flatapi';
const res = await fetch('/api/posts/page/1.json');
const page: PaginatedResponse<Posts> = await res.json();
// page.data[0].title — fully typedOptionally validate records at build time using JSON Schema. Add a schema path to your collection config:
collections:
posts:
source: data/posts/
schema: schemas/post.jsonRecords that fail validation are skipped with a warning. The build continues with valid records.
Use flatapi validate in CI to check data quality without building:
# In a PR check workflow
- run: npx flatapi validate --config flatapi.config.ymlThis exits with code 1 if any collection has schema violations, missing sources, or broken relations — useful as a PR status check to gate data contributions.
| Command | Description |
|---|---|
flatapi build |
Build the API from your config |
flatapi dev |
Start a dev server with auto-rebuild on file changes |
flatapi validate |
Check config and data without building (exit code 0/1) |
flatapi types |
Generate TypeScript type definitions |
flatapi init |
Create a starter config file |
| Flag | Description |
|---|---|
-c, --config <path> |
Config file path (default: flatapi.config.yml) |
-o, --output <path> |
Output directory (overrides config) |
-s, --silent |
Suppress console output |
-p, --port <number> |
Dev server port (default: 4567) |
MIT