typescript-scaffolder
Version:
 ### Unit Test Coverage: 97.12%
761 lines (626 loc) • 23 kB
Markdown
# TypeScript Scaffolder

### Unit Test Coverage: 97.12%
`typescript-scaffolder` is a utility that creates TypeScript interfaces, enums, and config accessors from structured inputs like JSON, .env files, or interface definitions.
Ideal for API integrations that expose schema via JSON — just drop the file in and generate clean, typed code for full-stack use. You can also integrate this into CI pipelines or dev scripts to keep generated types in sync with your schemas.
## Features
- Generate TypeScript interfaces from JSON or schemas
- Generate JSON schemas from TypeScript interfaces
- Typed `.env` accessor generator
- Auto-create enums from interface keys
- Typed axios client api generation
- Typed client api helpers
- Command sequence generator
- Typed express server and client webhook generation
- Preserves directory structure
To view the full documentation, please visit <br>
https://eric-famanas.super.site/the-typescript-scaffolder
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [CLI Usage Examples](#cli-usage-examples)
- [Interface Generation](#interface-generation)
- [Environment Variable Interface](#environment-variable-interface)
- [Client Api Generation](#api-client-generation-from-interface)
- [Webhook Server Generation](#webhook-Server-generation-from-interface)
- [Roadmap](#roadmap)
- [Reporting Bugs](#reporting-bugs)
- [Contributing](#contributing)
### Interface Generation
Generate TypeScript interfaces automatically from JSON schemas or raw JSON data.
- Infers full TypeScript interfaces using [quicktype](https://github.com/quicktype/quicktype)
- Supports nested objects and arrays
- Preserves directory structure from i.e. `schemas/<folder_name>` into `codegen/interfaces/<folder_name>`
- Automatically creates output folders if they don't exist
This file:
```
{
"id": "u_123",
"email": "alice@example.com",
"age": 29,
"isActive": true,
"roles": ["admin", "editor"],
"preferences": {
"newsletter": true,
"theme": "dark"
},
"lastLogin": "2024-12-01T10:15:30Z"
}
```
Will give you:
```
export interface User {
id: string;
email: string;
age: number;
isActive: boolean;
roles: string[];
preferences: Preferences;
lastLogin: Date;
}
export interface Preferences {
newsletter: boolean;
theme: string;
}
```
It will also format out nested objects, where this file
```
{
"records": [
{
"userID": 101,
"sessionKey": "abc123",
"events": [
{
"eventID": 1,
"status": "ok",
"code": 200,
"message": "Success"
}
],
"status": "ok",
"code": 200,
"message": "Success"
}
],
"status": "ok",
"code": 200,
"message": "Success"
}
```
Will give you:
```
export interface ApiResponse {
records: Record[];
status: string;
code: number;
message: string;
}
export interface Record {
userID: number;
sessionKey: string;
events: Event[];
status: string;
code: number;
message: string;
}
export interface Event {
eventID: number;
status: string;
code: number;
message: string;
}
```
### Environment Variable Interface
- Reduces need for calling dotenv.config() from multiple areas
- Creates an environment variable handler
- Automatically generated based on keys declared in .env file
- Automatically creates default values based on declared in .env file
- Supports filenames such as .env.local or .env.prod as well
This file:
```
SCHEMAS_DIR="schemas"
OUTPUT_DIR_ROOT="codegen"
INTERFACES_ROOT="interfaces"
```
Will give you:
```
export class EnvConfig {
static readonly SCHEMAS_DIR: string = process.env.SCHEMAS_DIR ?? '"schemas"';
static readonly OUTPUT_DIR_ROOT: string = process.env.OUTPUT_DIR_ROOT ?? '"codegen"';
static readonly INTERFACES_ROOT: string = process.env.INTERFACES_ROOT ?? '"interfaces"';
}
export enum EnvKeys {
SCHEMAS_DIR = "SCHEMAS_DIR",
OUTPUT_DIR_ROOT = "OUTPUT_DIR_ROOT",
INTERFACES_ROOT = "INTERFACES_ROOT"
}
```
### API Client generation from interface
Generate TypeScript `GET_ALL, GET, POST, PUT, DELETE` REST Api client based on a configuration file
that uses referenced interfaces for typing.
### Directory Structure Notes
When using `generateApiClientsFromPath`, follow this pattern for best results:
- Config files should be stored in a **flat** folder structure (e.g., `config/endpoint-configs`)
- Generated interfaces can be stored in a **parent folder with subdirectories** to reflect source groupings (e.g., `codegen/interfaces/source-a`, `codegen/interfaces/source-b`)
- The output directory (e.g., `codegen/apis`) will **mirror** the structure of the interfaces directory
- The names specified in the api config file must match the interface file name.
For example, given:
- Interface input input: `codegen/interfaces/source-charlie/GET_RES_users.ts`
- Config file: `config/endpoint-configs/users.json`
- Config responseSchema value: `GET_RES_users`
- Output file: `codegen/apis/source-charlie/USESR_api.ts`
The following interface is used to define the api config
```
export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
export type AuthType = 'basic' | 'apikey' | 'none'
export interface Endpoint {
method: Method;
path: string;
objectName: string;
operationName?: string;
pathParams?: string[];
queryParams?: string[];
headers?: Record<string, string>;
requestSchema?: string;
responseSchema: string;
}
export type EndpointConfigType = {
baseUrl: string
endpoints: Endpoint[]
};
export interface EndpointAuthConfig {
authType: AuthType;
credentials?: {
username?: string;
password?: string;
apiKeyName?: string;
apiKeyValue?: string;
};
}
export interface EndpointClientConfigFile extends EndpointConfigType, EndpointAuthConfig {
}
```
As an example, if you have interfaces generated from the following JSON files:
```
GET_RES_people.json // defines an array
GET_RES_person.json // defines a single object
POST_REQ_create_person.json // defines a single object for creation
```
And you define your JSON config as below:
```
{
"baseUrl": "https://api.example.com",
"endpoints": [
{
"method": "GET",
"path": "/people",
"responseSchema": "GET_RES_people",
"pathParams": [],
"objectName": "person"
},
{
"method": "GET",
"path": "/people/:id",
"responseSchema": "GET_RES_person",
"pathParams": ["id"],
"objectName": "person"
},
{
"method": "POST",
"path": "/people",
"requestSchema": "POST_REQ_create_person",
"responseSchema": "GET_RES_person",
"pathParams": [],
"objectName": "person"
},
{
"method": "PUT",
"path": "/people/:id",
"requestSchema": "POST_REQ_create_person",
"responseSchema": "GET_RES_person",
"pathParams": ["id"],
"objectName": "person"
},
{
"method": "DELETE",
"path": "/people/:id",
"responseSchema": "GET_RES_person",
"pathParams": ["id"],
"objectName": "person"
}
],
"authType": "apikey",
"credentials": {
"apiKeyName": "x-api-key",
"apiKeyValue": "test-1234"
}
}
```
The system will produce a file called person_api.ts
```
import { GET_RES_people } from "../../interfaces/source-charlie/GET_RES_people";
import axios from "axios";
import { GET_RES_person } from "../../interfaces/source-charlie/GET_RES_person";
import { POST_REQ_create_person } from "../../interfaces/source-charlie/POST_REQ_create_person";
export async function GET_ALL_person(headers?: Record<string, string>): Promise<GET_RES_people> {
const authHeaders = { "x-api-key": "test-1234" };
const response = await axios.get(
`https://api.example.com/people`,
{
headers: {
...authHeaders,
...headers,
},
}
);
return response.data;
}
export async function GET_person(id: string, headers?: Record<string, string>): Promise<GET_RES_person> {
const authHeaders = { "x-api-key": "test-1234" };
const response = await axios.get(
`https://api.example.com/people/${id}`,
{
headers: {
...authHeaders,
...headers,
},
}
);
return response.data;
}
export async function POST_person(body: POST_REQ_create_person, headers?: Record<string, string>): Promise<GET_RES_person> {
const authHeaders = { "x-api-key": "test-1234" };
const response = await axios.post(
`https://api.example.com/people`,
body,
{
headers: {
...authHeaders,
...headers,
},
}
);
return response.data;
}
export async function PUT_person(id: string, body: POST_REQ_create_person, headers?: Record<string, string>): Promise<GET_RES_person> {
const authHeaders = { "x-api-key": "test-1234" };
const response = await axios.put(
`https://api.example.com/people/${id}`,
body,
{
headers: {
...authHeaders,
...headers,
},
}
);
return response.data;
}
export async function DELETE_person(id: string, headers?: Record<string, string>): Promise<GET_RES_person> {
const authHeaders = { "x-api-key": "test-1234" };
const response = await axios.delete(
`https://api.example.com/people/${id}`,
{
headers: {
...authHeaders,
...headers,
},
}
);
return response.data;
}
```
### Webhook Server Generation from interface
When using `generateWebhookAppFromPath`, follow this pattern for best results:
- Config files should be stored in a **flat** folder structure (e.g., `config/webhook-configs`)
- Generated interfaces can be stored in a **parent folder with subdirectories** to reflect source groupings (e.g., `codegen/interfaces/source-a`, `codegen/interfaces/source-b`)
- The output directory (e.g., `codegen/webhooks`) will **mirror** the structure of the interfaces directory
- The names specified in the webhook config file must match the interface file name.
For example, given:
- Interface input input: `codegen/interfaces/source-echo/StripeWebhookPayload.ts`
- Config file: `config/webhook-configs/payment.json`
- Config responseSchema value: `StripeWebhookPayload`
- Output files:
- `codegen/webhooks/source-echo/webhookAppRegistry.ts`
- `codegen/webhooks/source-echo/createSourceEchoWebhookApp.ts`
- `codegen/webhooks/source-echo/routes/router.ts`
- `codegen/webhooks/source-echo/routes/handle_stripePaymentWebhook.ts`
The following interface is used to define the webhook config
```
export interface IncomingWebhook extends BaseWebhook {
direction: 'incoming'
path: string; // Required for incoming
handlerName: string; // required for route + handler generation
}
export interface OutgoingWebhook extends BaseWebhook {
direction: 'outgoing'
targetUrl: string; // Required for outgoing
}
export interface BaseWebhook extends SchemaConsumer {
direction: 'incoming' | 'outgoing';
name: string;
requestSchema: string;
responseSchema?: string;
headers?: Record<string, string>;
secretVerificationKey?: string; // Optional: used for signature validation
}
export type Webhook = IncomingWebhook | OutgoingWebhook;
export interface WebhookConfigFile {
webhooks: Webhook[];
}
```
As an example, if you have interfaces generated from the following JSON files:
```
// StripeWebhookPayload.json (incoming)
{
"id": "evt_12345",
"object": "event",
"type": "payment_intent.succeeded",
"data": {
"object": {
"amount": 2000,
"currency": "usd",
"status": "succeeded"
}
}
}
```
And you define your JSON config as below:
```
{
"webhooks": [
{
"direction": "incoming",
"name": "stripe_payment",
"path": "/webhooks/stripe",
"requestSchema": "StripeWebhookPayload",
"handlerName": "stripePaymentWebhook"
}
]
}
```
The system will produce the following files:
```
// webhookAppRegistry.ts
import * as Router from './routes/router';
import * as handle_stripePaymentWebhook from './routes/handle_stripePaymentWebhook';
export const webhookAppRegistry = {
'source-echo': {
router: Router,
handlers: {
...handle_stripePaymentWebhook
}
}
};
// createSourceEchoWebhookApp.ts
import express from "express";
import { webhookAppRegistry } from "./webhookAppRegistry";
export function createSourceEchoWebhookApp() {
const app = express();
app.use(express.json());
const handlers = webhookAppRegistry['source-echo']?.handlers || {};
for (const key of Object.keys(handlers)) {
app.post('/' + key, handlers[key]);
}
return app;
}
// routes/handle_stripePaymentWebhook.ts
import { StripeWebhookPayload } from "../../../interfaces/source-echo/StripeWebhookPayload";
export async function handleStripePaymentWebhookWebhook(payload: StripeWebhookPayload): Promise<void> {
// TODO: Implement webhook handler logic here
console.log("Received webhook payload:", payload);
}
// routes/router.ts
import express from "express";
import type { StripeWebhookPayload } from "../../../interfaces/source-echo/StripeWebhookPayload";
import { handleStripePaymentWebhookWebhook } from "./handle_stripePaymentWebhook";
const router = express.Router();
router.use(express.json());
router.post('/webhooks/stripe', async (req, res) => {
try {
const payload = req.body as StripeWebhookPayload;
await handleStripePaymentWebhookWebhook(payload);
res.status(200).json({ ok: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ ok: false });
}
});
export default router;
```
Alternatively, if you express an outgoing webhook, it will look like this
```
import { NotifyPayload } from "../../../interfaces/source-foxtrot/NotifyPayload";
import { NotifyResponse } from "../../../interfaces/source-foxtrot/NotifyResponse";
import axios from "axios";
import { AxiosRequestConfig } from "axios";
export async function sendNotifyPartnerWebhook(body: NotifyPayload, headers?: Record<string, string>): Promise<NotifyResponse> {
const response = await axios.post(`https://partner.example.com/webhook`, body, { headers });
return response.data;
}
```
You can access the individual handlers by referencing the registry, e.g.
```
webhookAppRegistry['source-echo'].handlers.handle_stripePaymentWebhook
```
#### Webhook Test Routes and Fixtures (Beta)
When generating webhook server code, the generator now also produces a **test route** and a **fixture** for each webhook defined in your config.
- **Test Routes**: For every incoming webhook, a matching test route is generated alongside your router. This route can be used during development to send a mock payload to your handler without relying on the real external webhook source.
- **Fixtures**: For each request schema referenced in your webhook config, a `<InterfaceName>.fixture.ts` file is generated. This fixture contains a typed example payload matching the interface and can be used to drive unit tests or manual route testing.
The fixture generator will:
- Parse the referenced TypeScript interface to generate representative values for all fields.
- Use name-based faker heuristics for realistic values where possible, otherwise fall back to generic fakers.
- Handle nested objects, arrays, enums, unions, optional fields, and basic type inference.
The generated test routes import and send these fixtures to your handlers, providing an end-to-end loop for quickly verifying that your webhook handling logic and type expectations are correct.
> **Note:** This feature is currently in beta. Fixture generation is best-effort and may require manual adjustment for complex or domain-specific payloads.
## Installation
To install the package, run the following command
```
npm install typescript-scaffolder
```
---
## Usage
### IMPORTANT: Considerations for where to place generated code
If you intend to import the generated output into your main application code (e.g., use interfaces or API clients),
we recommend placing the `codegen/` directory inside your `src/` folder.
For example:
```
src/
codegen/
apis/
apis/sequences
config/
enums/
interfaces/
webhooks/
```
This ensures:
- TypeScript includes the generated files in your compilation scope
- IDE tools (like IntelliSense or import resolution) behave correctly
- You avoid pathing issues or brittle import warnings
If you keep `codegen/` outside of `src/`, you may need to update your `tsconfig.json` to include it,
or manually relocate usable outputs into `src/` after generation.
### Example usages
Please refer to the following code block for example usages:
```
const ROOT_DIR = process.cwd(); // Base dir where the script is run
const LOCAL_DIR = __dirname; // Base dir where this file lives
const CODEGEN_DIR = path.resolve(LOCAL_DIR, 'src/codegen')
// Interface generation config
const SCHEMA_INPUT_DIR = path.resolve(LOCAL_DIR, 'config/schemas');
const INTERFACE_OUTPUT_DIR = path.resolve(CODEGEN_DIR, 'interfaces');
// Generate enums, this will use the previously generated interface output
const ENUM_OUTPUT_DIR = path.resolve(CODEGEN_DIR, 'enums');
// Client endpoint generation config
const ENDPOINT_CONFIG_PATH = path.resolve(LOCAL_DIR, 'config/endpoint-configs');
const CLIENT_OUTPUT_DIR = path.resolve(CODEGEN_DIR, 'apis')
// Webhook server generation config
const WEBHOOK_CONFIG_PATH = path.resolve(LOCAL_DIR, 'config/webhook-configs');
const WEBHOOK_OUTPUT_DIR = path.resolve(CODEGEN_DIR, 'webhooks');
// Env accessor config
const ENV_FILE = path.resolve(ROOT_DIR, '.env');
const ENV_OUTPUT_DIR = path.resolve(CODEGEN_DIR, 'config');
const ENV_OUTPUT_FILE = 'env-config.ts';
async function build() {
// using the env accessor
// this is a sync function, and should be run first anyway
generateEnvLoader(ENV_FILE, ENV_OUTPUT_DIR, ENV_OUTPUT_FILE);
// using the interface generator
await generateInterfacesFromPath(SCHEMA_INPUT_DIR, INTERFACE_OUTPUT_DIR)
// use the enum generator from the output of the interface generator
await generateEnumsFromPath(INTERFACE_OUTPUT_DIR, ENUM_OUTPUT_DIR);
// Generates an object-centric axios api client based on a config file
await generateApiClientsFromPath(ENDPOINT_CONFIG_PATH, INTERFACE_OUTPUT_DIR, CLIENT_OUTPUT_DIR);
// Generate the api registry to access the generated client functions
await generateApiRegistry(CLIENT_OUTPUT_DIR);
// Generates a command sequence file based on the generated client-api registry
await generateSequencesFromPath(SEQUENCE_CONFIG_PATH, CLIENT_OUTPUT_DIR);
// Generate an express webhook application
await generateWebhookAppFromPath(WEBHOOK_CONFIG_PATH, INTERFACE_OUTPUT_DIR, WEBHOOK_OUTPUT_DIR)
}
build();
```
## CLI Usage Examples
Below are example commands to run each of the CLI subcommands available in `typescript-scaffolder`.
### Generate Interfaces
Generate TypeScript interfaces from JSON schema files:
```bash
typescript-scaffolder interfaces \
--input ./schemas \
--output ./codegen/interfaces
```
### Generate Enums
Generate TypeScript enums from interface files:
```bash
typescript-scaffolder enums \
--input ./codegen/interfaces \
--output ./codegen/enums \
--ext .ts
```
### Generate Environment Loader
Generate a typed environment variable accessor from a `.env` file:
```bash
typescript-scaffolder envloader \
--env-file .env \
--output-dir ./codegen/config \
--output-file env-config.ts \
--class-name EnvConfig \
--enum-name EnvKeys
```
### Generate API Client From Single Config File
Generate an API client from a single JSON config file:
```bash
typescript-scaffolder apiclient-file \
--config ./config/api.json \
--interfaces ./codegen/interfaces \
--output ./codegen/apis
```
### Generate API Clients From Config Directory
Generate API clients from multiple JSON config files in a directory:
```bash
typescript-scaffolder apiclient-dir \
--config-dir ./config/api-configs \
--interfaces-root ./codegen/interfaces \
--output-root ./codegen/apis
```
### Generate API Client Registry
Generate a consolidated API client registry file from generated clients:
```bash
typescript-scaffolder apiclient-registry \
--api-root ./codegen/apis \
--registry-file registry.ts
```
### Generate Sequence Runner
Generate command functions to call certain API points in sequence
```bash
typescript-scaffolder sequences \
--config-dir ./config/sequences \
--output ./src/codegen/apis/sequences
```
### Generate Webhook App
Generate an Express webhook app and registry from a config file:
```bash
typescript-scaffolder webhooks \
--config ./config/source-bravo.json \
--interfaces ./codegen/interfaces \
--output ./codegen/webhooks
```
---
To see all available commands and options, run:
```bash
typescript-scaffolder help
```
or
```bash
typescript-scaffolder --help
```
## Roadmap
- [x] Generate TypeScript interfaces from schema definitions
- [x] Generate TypeScript enums to assert key names
- [x] Generate TypeScript accessor for environment variables
- [x] Generate TypeScript axios REST api client from interfaces
- [x] Generate Typescript command sequences for REST api calls
- [x] Generate Typescript axios client webhook apps
- [x] Generate Typescript helper functions for REST api calls
- [x] Generate Typescript webhook test routes and fixtures
- [x] Generate Typescript express server webhook apps
- [x] Command line interface access
- [ ] Factory classes based on interfaces
- [ ] Scaffolding for service mocking (GET, POST, PUT, DELETE)
- [ ] Generate enums from definitions
- [ ] Generate classes from schema definitions
- [ ] Declarative function generation
## Reporting Bugs
If you encounter a bug or unexpected behavior, please open an issue with:
- A clear description of the problem
- Steps to reproduce it (code snippets, input files, etc.)
- The expected vs. actual result
- Your environment (OS, Node.js version, TypeScript version)
Bug reports are appreciated and help improve the project, even if you're not submitting a fix directly.
## Contributing
This project is currently maintained as a solo project. While issues and ideas are welcome, I’m not accepting external pull requests at this time.
## Repo
https://github.com/ejfamanas/typescript-scaffolder
## License
Licensed under the [MIT License](LICENSE).