Skip to content

dvelton/flatapi

Repository files navigation

FlatAPI

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.

What You Get

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

Quick Start

1. Install

npm install -g flatapi

2. Create a config file

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]

3. Add your data

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.

4. Build

flatapi build

This creates an api/ directory with all your endpoints. Serve it with any static file server, or deploy to GitHub Pages.

Local Development

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

Using with GitHub Pages

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@v4

Every push to main rebuilds and deploys your API.

Using with an existing Pages site

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.

Config Reference

Top-level options

Option Default Description
output api Directory where generated files are written
per_page 20 Default number of records per page

Collection options

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.

Relationships

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"
}

Consuming the API

Paginated listing

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 }

Filtered view

const res = await fetch('https://you.github.io/api/posts/by-category/tutorial.json');
const { data, meta } = await res.json();

Alternative sort order

const res = await fetch('https://you.github.io/api/posts/sort-by/title-asc/page/1.json');
const { data, meta } = await res.json();

Full-text search

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 }
}

Aggregate statistics

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" }

Change history

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: [...] }

SQLite queries in the browser

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"
);

TypeScript Support

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 typed

Schema Validation

Optionally validate records at build time using JSON Schema. Add a schema path to your collection config:

collections:
  posts:
    source: data/posts/
    schema: schemas/post.json

Records 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.yml

This 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.

CLI Commands

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

Common flags

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)

License

MIT

About

Turn data files in your repo into a queryable static API

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors