npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mdxui/api

v6.0.0

Published

JSON/HATEOAS renderer for MDXUI - expose your UI as a navigable API

Downloads

50

Readme

@mdxui/api

JSON/HATEOAS renderer for MDXUI. Turn your UI components into a navigable REST API with automatic OpenAPI documentation generation.

Features

  • JSON Rendering - Convert React component trees to JSON
  • HATEOAS Links - Generate navigation links for API discoverability
  • OpenAPI Generation - Auto-generate OpenAPI 3.1 specs from routes
  • Content Negotiation - Support for JSON, HAL+JSON, and JSON:API formats
  • Caching Utilities - ETag and Cache-Control header generation

Installation

pnpm add @mdxui/api @mdxui/navigation

Quick Start

1. Define Your Routes

import { createRoutes } from '@mdxui/navigation'
import { z } from 'zod'

const routes = createRoutes({
  users: {
    path: '/users',
    params: z.object({
      limit: z.number().int().min(1).max(100).optional(),
    }),
    meta: {
      summary: 'List all users',
      responseSchema: UserSchema,
      schemaName: 'User',
      isList: true,
    },
    children: {
      detail: {
        path: '/:id',
        params: z.object({ id: z.string().uuid() }),
        meta: { summary: 'Get user by ID' },
      },
    },
  },
})

2. Render to JSON

import { renderToJSON, renderListView, renderDetailView } from '@mdxui/api'

// Simple component rendering
const json = renderToJSON(<UserCard user={user} />)
// => { type: 'UserCard', props: { user: {...} }, children: [...] }

// List view with HATEOAS links
const response = renderListView(users, {
  route: routes.users,
  params: {},
  baseUrl: 'https://api.example.com.ai',
  itemKey: 'id',
  pagination: { cursor: 'abc', limit: 20, hasMore: true },
})
// => { meta: {...}, data: [...], _links: { self, parent, children, next } }

// Detail view with HATEOAS links
const detail = renderDetailView(user, {
  route: routes.users.children.detail,
  params: { id: '123' },
  baseUrl: 'https://api.example.com.ai',
  actions: ['update', 'delete'],
})

3. Generate OpenAPI Spec

import { generateOpenApi } from '@mdxui/api/openapi'

const spec = generateOpenApi(routes, {
  title: 'My API',
  version: '1.0.0',
  description: 'User management API',
  servers: [{ url: 'https://api.example.com.ai', description: 'Production' }],
})

// Serve at /openapi.json
app.get('/openapi.json', (c) => c.json(spec))

4. Build HATEOAS Links

import { createLinkBuilder } from '@mdxui/api/hateoas'

const builder = createLinkBuilder({
  routes,
  baseUrl: 'https://api.example.com.ai',
})

// Navigation links for a route
const links = builder.forRoute(routes.users.children.detail, { id: '123' })
// => {
//   self: { href: 'https://api.example.com.ai/users/123', rel: 'self' },
//   parent: { href: 'https://api.example.com.ai/users', rel: 'parent' },
//   children: [{ href: '.../settings', rel: 'settings' }, ...],
//   siblings: [{ href: '.../profile', rel: 'profile' }]
// }

// Pagination links
const pagination = builder.forPagination(routes.users, {}, {
  cursor: 'page2',
  limit: 20,
  hasMore: true,
})

// Action links
const actions = builder.forActions(routes.users.children.detail, { id: '123' }, [
  { name: 'update', method: 'PUT' },
  { name: 'delete', method: 'DELETE' },
])

API Reference

Main Exports

renderToJSON(element, context?)

Render a React element to JSON structure.

// Without context - returns raw JSONNode
const json = renderToJSON(<Component />)
// => { type: 'Component', props: {...}, children: [...] }

// With context - returns full ApiResponse with HATEOAS links
const response = renderToJSON(<UserDetail user={user} />, {
  route: routes.users.children.detail,
  params: { id: '123' },
  baseUrl: 'https://api.example.com.ai',
})
// => { meta: {...}, data: {...}, _links: {...} }

renderListView(items, options)

Render a list/collection view with automatic HATEOAS links.

const response = renderListView(users, {
  route: routes.users,
  params: {},
  baseUrl: 'https://api.example.com.ai',
  itemKey: 'id',
  pagination: {
    cursor: 'next-page-token',
    prevCursor: 'prev-page-token',
    limit: 20,
    hasMore: true,
    total: 100,
  },
})

renderDetailView(item, options)

Render a single resource view with HATEOAS links.

const response = renderDetailView(user, {
  route: routes.users.children.detail,
  params: { id: '123' },
  baseUrl: 'https://api.example.com.ai',
  actions: ['update', 'delete'],
})

API(options) (In Development)

Create an HTTP request handler for your API.

import { API } from '@mdxui/api'

const handler = API({
  routes,
  baseUrl: 'https://api.example.com.ai',
  getData: async (route, params) => {
    // Fetch data for the route
  },
})

// Use with Hono, Express, etc.
app.get('/api/*', handler)

OpenAPI Exports (@mdxui/api/openapi)

generateOpenApi(routes, options)

Generate an OpenAPI 3.1 specification from route definitions.

const spec = generateOpenApi(routes, {
  title: 'My API',
  version: '1.0.0',
  description: 'API description',
  servers: [{ url: 'https://api.example.com.ai' }],
  basePath: '/api/v1', // Optional path prefix
})

Features:

  • Converts :param to {param} format
  • Extracts parameters from Zod schemas
  • Generates response schemas with HATEOAS structure
  • Includes error responses (400, 404, 500) for routes with path params
  • Supports meta.tags, meta.summary, meta.description
  • Registers component schemas from meta.responseSchema

zodToJsonSchema(schema)

Convert a Zod schema to JSON Schema format.

const jsonSchema = zodToJsonSchema(z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  role: z.enum(['admin', 'user']),
}))

HATEOAS Exports (@mdxui/api/hateoas)

createLinkBuilder(options)

Create a builder for generating HATEOAS navigation links.

const builder = createLinkBuilder({
  routes,
  baseUrl: 'https://api.example.com.ai',
})

// Methods:
builder.forRoute(route, params)      // Navigation links
builder.forPagination(route, params, paginationInfo)  // Pagination links
builder.forActions(route, params, actions)  // Action links

formatResponse(data, links, contentType)

Format a response based on content type.

import { formatResponse, negotiateContentType } from '@mdxui/api/hateoas'

// Determine content type from Accept header
const contentType = negotiateContentType(
  request.headers.get('Accept'),
  ['application/json', 'application/hal+json', 'application/vnd.api+json'],
  'application/json'
)

const response = formatResponse(data, links, contentType)

parseAcceptHeader(header)

Parse an HTTP Accept header into sorted media types.

const types = parseAcceptHeader('application/json, application/hal+json;q=0.9')
// => [
//   { type: 'application/json', quality: 1, params: {} },
//   { type: 'application/hal+json', quality: 0.9, params: {} }
// ]

Caching Utilities

import { generateETag, generateCacheControl, cachePresets, matchesETag } from '@mdxui/api/hateoas'

// Generate ETag from content
const etag = generateETag(responseData)
// => 'W/"a1b2c3d4"'

// Check if client cache is fresh
if (matchesETag(request.headers.get('If-None-Match'), etag)) {
  return new Response(null, { status: 304 })
}

// Generate Cache-Control header
const cacheControl = generateCacheControl({
  public: true,
  maxAge: 3600,
  staleWhileRevalidate: 60,
})
// => 'public, max-age=3600, stale-while-revalidate=60'

// Use presets
generateCacheControl(cachePresets.apiResponse(60))
generateCacheControl(cachePresets.staticAsset())
generateCacheControl(cachePresets.noCache())

JSON:API Helpers

import { toJsonApiResource, toJsonApiCollection, toJsonApiLinks } from '@mdxui/api/hateoas'

// Convert object to JSON:API resource
const resource = toJsonApiResource(user, 'users')
// => { type: 'users', id: '123', attributes: { name: 'Alice', ... } }

// Convert array to JSON:API collection
const collection = toJsonApiCollection(users, 'users', {
  links: { self: '/users' },
  meta: { total: 100 },
})

Response Formats

JSON (default)

Standard JSON with _links object:

{
  "meta": {
    "view": "/users/:id",
    "params": { "id": "123" },
    "timestamp": "2024-01-01T00:00:00.000Z"
  },
  "data": {
    "id": "123",
    "name": "Alice",
    "email": "[email protected]"
  },
  "_links": {
    "self": "/users/123",
    "parent": "/users",
    "schema": "/users/schema"
  }
}

HAL+JSON (application/hal+json)

Data with embedded _links:

{
  "id": "123",
  "name": "Alice",
  "email": "[email protected]",
  "_links": {
    "self": { "href": "/users/123", "rel": "self", "method": "GET" },
    "parent": { "href": "/users", "rel": "parent", "method": "GET" }
  }
}

JSON:API (application/vnd.api+json)

{
  "data": {
    "type": "users",
    "id": "123",
    "attributes": {
      "name": "Alice",
      "email": "[email protected]"
    }
  },
  "links": {
    "self": "/users/123",
    "related": "/users"
  }
}

Examples

Basic List/Detail API

import { Hono } from 'hono'
import { createRoutes } from '@mdxui/navigation'
import { renderListView, renderDetailView, generateOpenApi, createLinkBuilder } from '@mdxui/api'
import { z } from 'zod'

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
})

const routes = createRoutes({
  users: {
    path: '/users',
    meta: { responseSchema: UserSchema, schemaName: 'User', isList: true },
    children: {
      detail: {
        path: '/:id',
        params: z.object({ id: z.string().uuid() }),
        meta: { responseSchema: UserSchema, schemaName: 'User' },
      },
    },
  },
})

const app = new Hono()
const baseUrl = 'https://api.example.com.ai'

// OpenAPI spec
app.get('/openapi.json', (c) => c.json(generateOpenApi(routes, {
  title: 'User API',
  version: '1.0.0',
})))

// List users
app.get('/users', async (c) => {
  const users = await db.users.findMany({ take: 20 })
  return c.json(renderListView(users, {
    route: routes.users,
    params: {},
    baseUrl,
    itemKey: 'id',
  }))
})

// Get user by ID
app.get('/users/:id', async (c) => {
  const user = await db.users.findUnique({ where: { id: c.req.param('id') } })
  if (!user) return c.json({ error: 'Not found' }, 404)

  return c.json(renderDetailView(user, {
    route: routes.users.children.detail,
    params: { id: user.id },
    baseUrl,
    actions: ['update', 'delete'],
  }))
})

export default app

With Pagination

app.get('/users', async (c) => {
  const cursor = c.req.query('cursor')
  const limit = parseInt(c.req.query('limit') || '20')

  const { items, nextCursor, prevCursor, total } = await db.users.paginate({
    cursor,
    limit,
  })

  return c.json(renderListView(items, {
    route: routes.users,
    params: {},
    baseUrl,
    itemKey: 'id',
    pagination: {
      cursor: nextCursor,
      prevCursor,
      limit,
      hasMore: !!nextCursor,
      total,
    },
  }))
})

Content Negotiation

import { formatResponse, negotiateContentType, createLinkBuilder } from '@mdxui/api/hateoas'

app.get('/users/:id', async (c) => {
  const user = await db.users.findUnique({ where: { id: c.req.param('id') } })

  const builder = createLinkBuilder({ routes, baseUrl })
  const links = builder.forRoute(routes.users.children.detail, { id: user.id })

  const contentType = negotiateContentType(
    c.req.header('Accept') || '',
    ['application/json', 'application/hal+json', 'application/vnd.api+json'],
    'application/json'
  )

  const response = formatResponse(user, links, contentType)
  return c.json(response, { headers: { 'Content-Type': contentType } })
})

With Caching

import { generateETag, generateCacheControl, matchesETag, cachePresets } from '@mdxui/api/hateoas'

app.get('/users/:id', async (c) => {
  const user = await db.users.findUnique({ where: { id: c.req.param('id') } })

  const etag = generateETag(user)
  const ifNoneMatch = c.req.header('If-None-Match')

  if (ifNoneMatch && matchesETag(ifNoneMatch, etag)) {
    return new Response(null, { status: 304 })
  }

  const response = renderDetailView(user, { route, params, baseUrl })

  return c.json(response, {
    headers: {
      'ETag': etag,
      'Cache-Control': generateCacheControl(cachePresets.apiResponse(60)),
    },
  })
})

TypeScript Types

import type {
  // Core types
  JSONNode,
  ApiResponse,
  ApiViewMeta,
  ApiLinks,
  RenderContext,
  RenderOptions,

  // OpenAPI types
  OpenApiSpec,
  OpenApiPath,
  OpenApiOperation,
  OpenApiParameter,
  GenerateOpenApiOptions,

  // HATEOAS types
  HateoasLink,
  HateoasLinks,
  LinkBuilderOptions,
  ContentType,
  CacheOptions,

  // JSON:API types
  JsonApiDocument,
  JsonApiResource,
  JsonApiLinks,
} from '@mdxui/api'

License

MIT