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

json-for-llm

v1.0.9

Published

Standardized JSON response format for LLM consumption with sensitive field filtering - NestJS + Prisma plugin

Readme

json-for-llm

A NestJS plugin that provides standardized JSON endpoints for LLM consumption. When an LLM agent sends a request with X-LLM-Ready: true, it receives structured data instead of HTML — without the agent needing to know your internal API shape.

Design Philosophy

Two rules, strictly enforced:

  1. The plugin never touches your database. It calls data-fetching functions you provide (fetcher), not Prisma directly. Your services remain the only place where database queries live.
  2. Adding new content types requires zero code changes. Drop a config entry, wire up the fetcher — done.

Features

  • Schema-agnostic: No coupling to schema.prisma model names
  • Sensitive field filtering: Strips passwords, tokens, API keys, etc. before responding
  • Standardized response format: Every content type returns the same LLMResponse structure
  • Frontend route interception: Agents use the same URLs as humans; no separate API paths needed
  • Nginx template included: One-line configuration for the reverse proxy

Installation

npm install json-for-llm

Requires NestJS ≥ 10 and a @Global() PrismaModule in your application (needed only for the Prisma fallback mode — see below).


Quick Start

1. Wire up the module

// app.module.ts
import { LlmProxyModule } from 'json-for-llm';
import { ArticlesService } from './modules/articles/articles.service';
import { EventsService } from './modules/events/events.service';
import { PersonsService } from './modules/persons/persons.service';

@Module({
  imports: [
    // ... your other modules
    LlmProxyModule.register({
      backendUrl: 'http://localhost:3000',
      models: [
        {
          route: 'articles',
          fetcher: (slug) => articlesService.findOne(slug),
          format: 'article',
        },
        {
          route: 'events',
          fetcher: (slug) => eventsService.findOne(slug),
          format: 'event',
        },
        {
          route: 'people',
          fetcher: (slug) => personsService.findOne(slug),
          format: 'profile',
        },
        // Add more content types here — no other code changes needed
      ],
    }),
  ],
})
export class AppModule {}

2. Update nginx

Forward X-LLM-Ready: true requests to NestJS:

location / {
    if ($http_x_llm_ready = "true") {
        proxy_pass http://backend_api;
    }
    try_files $uri $uri/ /index.html;
}

3. Done

LLM agents now get structured JSON from your existing URLs:

# Human request → HTML page (unchanged)
curl https://your-site.com/articles/my-post

# Agent request → structured JSON (new)
curl -H 'X-LLM-Ready: true' https://your-site.com/articles/my-post
# → {"llm":{...},"title":"...","content":{...},"metadata":{...}}

Auto-Registration (autoRegister)

If you prefer zero-configuration, LlmProxyModule.autoRegister() scans your running NestJS application and automatically discovers all content routes — no manual model list needed.

// app.module.ts (after bootstrap)
const llmModule = await LlmProxyModule.autoRegister(app, {
  backendUrl: 'http://localhost:3000',
  enableMiddleware: true,
});

@Module({
  imports: [
    // ... other modules
    llmModule,
  ],
})
export class AppModule {}

autoRegister inspects your application's internal module resolver to find all controllers with slug-parameter routes (e.g. /articles/:slug). For each discovered route it:

  1. Infers the route segment from the controller name (ArticlesController'articles')
  2. Finds the best matching service method (findOne, findBySlug, getById, etc.)
  3. Wires a fetcher that calls the method with the slug
  4. Infers the format from the route name

Internal routes (/api/, /llm/, /health) are skipped automatically.


Privacy: @LLMSensitive Decorator

Beyond field-name-based filtering, you can mark specific DTO/entity fields with the @LLMSensitive() decorator to ensure they are always excluded from LLM responses — even if their name is not in the default blocklist.

import { LLMSensitive } from 'json-for-llm';

class PersonDto {
  name: string;
  email: string;
  department: string;

  @LLMSensitive({ description: 'HR-only field' })
  performanceReview: string;

  @LLMSensitive()
  flaggedForReview: boolean;
}

Fields marked with @LLMSensitive() are removed from responses automatically, regardless of their name. The description option is stored in metadata for audit purposes but does not affect filtering behavior.

To use the decorator, ensure reflect-metadata is imported once in your application entry point:

// main.ts
import 'reflect-metadata';

How It Works

LLM Agent                       nginx                        NestJS
─────────                       ─────                        ──────
GET /articles/my-post
  + X-LLM-Ready: true    ──►   if header=true
                                proxy_pass ─────────────►  LlmProxyMiddleware
                                                                  │
                                      /articles/my-post    ◄── matches route
                                      ─────────────────────────► articlesService.findOne('my-post')
                                                                  │
                                                                  ▼
                                               LLMResponse JSON  ◄── buildLLMResponse()
                                               ←─────────────────┘
←────────────────────────────────────────────────────────────── {"llm":{...},"title":"..."}

Middleware intercepts all frontend URLs. If X-LLM-Ready: true is present, the request is handled by the plugin. Otherwise it passes through to your normal page rendering.


Configuration

LlmProxyModule.register(options)

| Option | Type | Default | Description | |--------|------|---------|-------------| | backendUrl | string | — | Required. Your backend base URL. | | models | ModelConfig[] | — | Required. Content types to expose. | | headerName | string | 'x-llm-ready' | HTTP header that triggers the LLM response. | | contentType | string | 'application/llm+json' | Response Content-Type. | | cacheControl | string | 'private, max-age=3600' | Cache-Control header value. | | enableMiddleware | boolean | true | Whether to intercept frontend routes. | | maxContentLength | number | 3000 | Truncate content after this many characters. | | wordsPerMinute | number | 500 | Reading speed used to compute reading_time_seconds. | | excludeFields | string[] | [password, token, ...] | Additional fields to strip from responses. |

ModelConfig

| Field | Type | Description | |-------|------|-------------| | route | string | Required. URL path segment (e.g., 'articles'). Empty string '' for root-level pages. | | fetcher | (slug: string) ⇒ Promise<any> | Preferred. Your service method. Return null for 404. | | model | string | Legacy. Prisma model name. Only used when fetcher is absent. | | slugField | string | Legacy. Database field for slug lookups (default: 'slug'). | | include | Record<string,any> | Legacy. Prisma include config for relations. | | select | Record<string,any> | Legacy. Prisma select config. | | format | 'article' \| 'event' \| 'profile' \| 'job' \| 'publication' \| 'page' | Normalizes metadata extraction for this type. | | transform | (data: any) ⇒ any | Reshape raw data before building the LLMResponse. | | excludeFields | string[] | Per-model field exclusions (merged with global). | | interceptFrontendRoutes | boolean | Set false to only expose via /llm/:model/:slug API. |


Response Format

All responses follow this structure:

{
  llm: {
    version: string;           // always "1.0.0"
    format: string;            // 'article' | 'event' | 'profile' | 'job' | 'publication' | 'page'
    generated_at: string;       // ISO 8601
    ttl_seconds: number;        // 3600
  },
  title: string;
  url: string;                  // absolute URL
  summary: string;              // first 500 chars of description/excerpt
  content: {
    type: 'markdown';
    text: string;              // truncated to maxContentLength
    footnotes: string[];
  },
  entities: any[];             // e.g. speakers for events, authors for publications
  metadata: {
    author?: string;
    published_at?: string;
    modified_at?: string;
    tags: string[];
    word_count: number;
    reading_time_seconds: number;
    language: 'zh' | 'en';
  },
  links: { url: string; text: string; internal: boolean }[];
  images: { url: string; alt: string; title?: string }[];
}

Per-Model Transform

Use transform to reshape your service's response into the fields the plugin expects:

{
  route: 'articles',
  fetcher: (slug) => articlesService.findOne(slug),
  format: 'article',
  transform: (article) => ({
    title: article.titleZh || article.title,
    summary: article.summary || article.excerpt || '',
    content: article.content || '',
    author: article.author?.name,
    tags: article.tags?.map(t => t.name),
    links: [
      ...(article.relatedLinks || []),
      { url: article.sourceUrl, text: 'Source', internal: false },
    ],
    images: article.images?.map(img => ({ url: img.url, alt: img.alt })),
  })),
}

The transform output is passed through the sensitive-field filter before building the final response.


Sensitive Field Filtering

Two mechanisms work together to protect sensitive data:

1. Default blocklist (field-name based)

The following fields are automatically excluded from all responses (unless explicitly allowed via excludeFields):

password, passwd, pwd, creditCard, creditCardNumber, cvv, cvc, ssn, socialSecurityNumber, secret, token, apiKey, privateKey, secretKey, casdoorId, authProvider

2. @LLMSensitive decorator (per-field, metadata-based)

Mark specific DTO fields with @LLMSensitive() to exclude them regardless of name. See Privacy: @LLMSensitive Decorator above.


Nginx Template

A ready-to-use nginx configuration is included:

cat node_modules/json-for-llm/dist/templates/nginx.conf.template

Key rule — forward LLM requests to NestJS:

location / {
    if ($http_x_llm_ready = "true") {
        proxy_pass http://backend_api;
    }
    try_files $uri $uri/ /index.html;
}

Why No Prisma Coupling?

The first version of this plugin directly called PrismaClient[modelName].findUnique(). This meant your schema.prisma model names had to match the config exactly, and changing your schema broke the plugin.

The current version avoids this entirely. You provide a fetcher function — any async function that takes a slug and returns data (or null). The plugin formats the response; your services own the database. Your schema is yours to evolve.

The only reason PrismaMode (model/include/select) still exists is as a shortcut for simple projects that don't have a service layer yet. For production use, fetcher is the recommended approach.


API Endpoints

When enableMiddleware: true (default), no separate API endpoints are needed — agents use the same URLs as human visitors.

For direct API access (optional):

| Method | Path | Description | |--------|------|-------------| | GET | /llm/:model/:slug | Fetch content by model name + slug | | GET | /llm/page/:slug | Fetch content with empty route prefix | | GET | /llm/health | Health check |

All endpoints require the X-LLM-Ready: true header.


License

MIT