Skip to main content

Codegen

frappe-codegen reads DocType metadata from a live Frappe site and writes TypeScript interfaces for frappe-js-client. Pass GeneratedDocTypes into createFrappeClient so db.getDoc('ToDo', name) infers the row instead of FrappeDoc<object>.

It is a separate package with its own release cycle. It depends on frappe-js-client; the client never depends on codegen.

Requires apiVersion: 2 (Frappe v15+). Meta is GET /api/v2/doctype/{doctype}/meta. On v1 / missing meta the fetch is wrapped: frappe-codegen requires Frappe v15+ REST API v2…. There is no classic-REST fallback.

The CLI binary is frappe-codegen. The same package also exports a programmatic API (this page). Generated TypeDoc lives under Codegen API in the sidebar.

Node 20+.

Install

pnpm add -D frappe-codegen

Config + env

Keep non-secrets in frappe-codegen.config.json. Credentials go in the environment. The CLI rejects a config file that contains apiKey, apiSecret, or api-key.

frappe-codegen.config.json
{
"url": "https://frappe.example.com",
"out": "src/generated/frappe-types.ts",
"includeHidden": true,
"followTables": true,
"doctypes": ["ToDo", "User"],
"modules": ["Desk"]
}

Allowed file keys: url, out, includeHidden, followTables, doctypes, modules. Other keys are ignored. Non-string entries in doctypes / modules are dropped. Non-boolean includeHidden / followTables are treated as unset. The file must be a JSON object (not null or an array).

export FRAPPE_URL="https://frappe.example.com" # optional if url is in the file
export FRAPPE_API_KEY="…"
export FRAPPE_API_SECRET="…"
pnpm exec frappe-codegen
package.json
{
"scripts": {
"codegen": "frappe-codegen"
}
}

Precedence

Flags > environment > config file.

--doctype / --module are unioned with the file lists (then de-duplicated). They do not replace them.

--include-labels, --include-doctype-map, --dry-run, and secrets are CLI/env only.

Env: FRAPPE_URL, FRAPPE_API_KEY, FRAPPE_API_SECRET. Empty env values are ignored.

If both key and secret are omitted, the CLI uses anonymous auth (only works if Guest can read DocType meta). Supplying only one of key/secret also falls back to anonymous.

Default output path: ./frappe-types.generated.ts. Parent directories are created.

SituationCommand
Installed in the projectpnpm exec frappe-codegen
npmnpx frappe-codegen
One-offpnpm dlx frappe-codegen
Helppnpm exec frappe-codegen --help

CLI reference

Required after merge: a site URL, and at least one DocType source (--doctype, --module, or config doctypes / modules).

FlagDefaultMeaning
-u, --url <url>env / configSite base URL
-d, --doctype <name>Repeatable
--module <name>Every DocType in that Frappe module. Repeatable
--api-key / --api-secretFRAPPE_API_KEY / FRAPPE_API_SECRETToken auth
-o, --out <path>./frappe-types.generated.tsOutput .ts
--config <path>./frappe-codegen.config.json if presentExplicit config path
--follow-tables / --no-follow-tablesfollow onTable / Table MultiSelect children
--include-hidden / --no-include-hiddenhidden onForm-hidden fields
--include-labels / --no-include-labelslabels on/** label */
--include-doctype-map / --no-include-doctype-mapmap onGeneratedDocTypes / GeneratedInserts
--dry-runoffPrint name<TAB>seed|child, no file
-h, --help

Boolean flags use Node --no-* (allowNegative).

--dry-run still fetches metadata (and children if follow is on). seed is a DocType you asked for; child came from a Table field. Progress goes to stderr; dry-run lines go to stdout.

A config path passed with --config is always loaded (it does not have to be the default filename). Without --config, ./frappe-codegen.config.json is used only if it exists.

CLI failures print frappe-codegen failed: … to stderr and set process.exitCode = 1. --help writes the usage text and exits 0.

--module Desk paginates DocType with filters: [['module', '=', 'Desk']]. No matches after resolve throws No DocTypes matched.

One-shot (no config file)

pnpm exec frappe-codegen \
--url https://frappe.example.com \
--api-key "$FRAPPE_API_KEY" --api-secret "$FRAPPE_API_SECRET" \
--doctype "ToDo" --doctype "User" \
--include-hidden \
--out src/generated/frappe-types.ts

What is emitted

Each DocType becomes:

export type ToDo = FrappeDoc<{ doctype: 'ToDo'; description: string /* … */ }>
export type ToDoInsert = FrappeInsert<ToDo>

If the DocType has Check fields, insert typing is FrappeInsert<Omit<ToDo, checks> & Partial<Pick<ToDo, checks>>> — Checks are required on read (0 | 1) and optional on insert.

Fields with reqd: 1 are required on the read type. Other fields are optional (?), except Checks (always required on read).

When the map is on:

export interface GeneratedDocTypes {
ToDo: ToDo
}
export interface GeneratedInserts {
ToDo: ToDoInsert
}

Map keys are the exact Frappe names ("Sales Order"). Pass the map as createFrappeClient<GeneratedDocTypes>(...).

A file header warns not to edit by hand. Each type has a short generated-from-DocType comment.

Interface names are TitleCase (Sales OrderSalesOrder; item-priceItemPrice; foo/barFooBar; Item (Variant)ItemVariant). Names that collide (HR Settings vs HRSettings) throw frappe-codegen: interface name collision: …. Names starting with a digit are prefixed _ (2FA Settings_2FASettings).

Field names that are not valid JS identifiers are quoted ("weird name": string).

Imports: FrappeDoc, FrappeInsert from frappe-js-client/types; Link is added if any Link< appears.

Labels containing */ are escaped so they cannot break out of the doc comment.

Fields that are skipped

  • Virtual (is_virtual)
  • Layout: Section / Column / Tab Break, Fold, Heading, Button, HTML, Image
  • Hidden only with --no-include-hidden (hidden fields are included by default because Frappe hidden controls form visibility, not whether the field exists on the document)
  • Empty fieldname

Password fields get a comment that GET is usually empty or masked; use db.getPassword. The comment is emitted even when labels are off.

A DocType with no data fields still emits doctype: "…".

Fieldtype → TypeScript

FrappeTypeScript
Check0 | 1
Selectstring-literal union from complete trimmed option lines; else string
Table / Table MultiSelectChild[] if the child DocType was generated, else FrappeDoc<Record<string, unknown>>[]
Link + optionsLink<"Target">
Link without optionsstring
Int, Float, Currency, Percent, Rating, Durationnumber
JSON, Geolocationunknown
Data, texts, editors, Password, Date/Datetime/Time, Attach*, Barcode, Color, Signature, Phone, Icon, Autocomplete, Read Only, Dynamic Linkstring
anything elseunknown

Select options are split on newlines and trimmed; each complete non-empty line becomes a literal, including commas and colons. Empty/whitespace-only options yield string.

--follow-tables (default) BFS-walks Table / Table MultiSelect options and generates those child DocTypes too. --no-follow-tables leaves child rows as FrappeDoc<Record<string, unknown>>[] unless you listed the child yourself.

Normalized meta also keeps istable / issingle (0/1); they do not change the emitted TypeScript.

Typed client

import { createFrappeClient } from 'frappe-js-client'
import type { GeneratedDocTypes, GeneratedInserts } from './generated/frappe-types'

const frappe = createFrappeClient<GeneratedDocTypes>({ url, auth })
const todo = await frappe.db.getDoc('ToDo', name)
await frappe.db.createDoc('ToDo', {
description: 'Follow up',
status: 'Open',
} satisfies GeneratedInserts['ToDo'])

Regenerate on DocType changes. Commit the generated file or produce it in CI — do not edit it by hand.

CI

- run: pnpm exec frappe-codegen --config frappe-codegen.config.json
env:
FRAPPE_URL: ${{ secrets.FRAPPE_URL }}
FRAPPE_API_KEY: ${{ secrets.FRAPPE_API_KEY }}
FRAPPE_API_SECRET: ${{ secrets.FRAPPE_API_SECRET }}

Point --out at a path your app already imports. Fail the job if codegen exits non-zero (missing DocTypes, v1 site, name collision).

Programmatic API

Use this from a build script instead of shelling out. You can use any client auth; the CLI only supports token or anonymous.

import { createFrappeClient, tokenAuth } from 'frappe-js-client'
import {
DEFAULT_CONFIG_NAME,
assertUniqueInterfaceNames,
fetchDocTypeMeta,
fetchDocTypeMetas,
fetchWithOptionalFollow,
findDefaultConfigPath,
followChildTables,
generateInterface,
generateModule,
loadConfigFile,
mergeConfig,
resolveDocTypes,
toInterfaceName,
} from 'frappe-codegen'

const path = findDefaultConfigPath() // DEFAULT_CONFIG_NAME in cwd, if present
const file = path ? loadConfigFile(path) : undefined
const config = mergeConfig(file, { doctypes: ['ToDo'] })

const client = createFrappeClient({
url: config.url!,
apiVersion: 2,
auth: tokenAuth({ apiKey: config.apiKey!, apiSecret: config.apiSecret! }),
})

const names = await resolveDocTypes(client, {
doctypes: config.doctypes,
modules: config.modules,
})
const metas = await fetchWithOptionalFollow(client, names, config.followTables)
assertUniqueInterfaceNames(metas)
const source = generateModule(metas, {
includeHidden: config.includeHidden,
includeLabels: config.includeLabels,
emitDocTypeMap: config.emitDocTypeMap,
})

Config

ExportRole
DEFAULT_CONFIG_NAME'frappe-codegen.config.json'
findDefaultConfigPath(cwd?)Path if the default file exists
loadConfigFile(path)Reads JSON; rejects secrets
mergeConfig(file, overlay)File, then env, then overlay. Union doctypes / modules
CodegenFileConfigFile shape
CliOverlayOverlay shape (CLI flags / script)
ResolvedCodegenConfigMerged result (out, booleans, dryRun, secrets, …)

mergeConfig defaults: out: './frappe-types.generated.ts', includeHidden: true, followTables: true, dryRun: false, emitDocTypeMap: true, includeLabels: true.

Meta

ExportRole
DocFieldfieldname, fieldtype, label?, options?, reqd?, hidden?, description?, is_virtual?
DocTypeMetaname, fields, istable?, issingle?
fetchDocTypeMeta(client, doctype)One DocType via db.getMeta
fetchDocTypeMetas(client, doctypes)Pool of 4 concurrent fetches

FeatureNotSupportedError / NotFoundError from getMeta are rethrown as Error with the v15+ / v2 message and { cause }. Other errors propagate unchanged.

Resolve

ExportRole
resolveDocTypes(client, { doctypes, modules })Union of explicit names + DocTypes in listed modules
followChildTables(client, seeds)BFS Table / Table MultiSelect children
fetchWithOptionalFollow(client, names, follow)Fetch seeds, then optionally follow

listDocTypesInModule and childTableDoctypes are internal (not on the package barrel).

Generate

ExportRole
toInterfaceName(doctype)Sales OrderSalesOrder
assertUniqueInterfaceNames(metas)Throws on collision
generateInterface(meta, allMetas?, opts?)One read type + insert alias
generateModule(metas, opts?)Full .ts module (calls assertUniqueInterfaceNames)
GenerateOptionsincludeHidden?, includeLabels?, emitDocTypeMap? (all default as CLI)

mapFieldType, selectLiterals, LAYOUT_FIELD_TYPES, and isTableFieldType are internal.

The CLI (parseCliArgs, main, boot) is the frappe-codegen binary, not a library export.