Introduction
In the previous article I described how I split larger TypeScript projects into several sub-projects, with a schemas package at the bottom that contains all shared types and runtime validators based on vts.
That setup works well, but it has one weakness: writing the schemas by hand. A typical vts schema looks like this:
export const UserSchema = Vts.object({
id: Vts.string({description: 'User identifier'}),
status: Vts.enum(StatusEnum),
profile: ProfileSchema,
}, {
description: 'User entity schema',
objectSchema: {
ignoreAdditionalItems: true
}
});
export type User = ExtractSchemaResultType<typeof UserSchema>;
That is a lot of boilerplate for what is essentially "User has an id, a status and a profile". When you have fifty of those, scattered across folders, with extends and cross-file references, it stops being fun. So I built a small tool that takes care of the boilerplate - VTS Editor.
What is VTS Editor?
VTS Editor is a browser-based visual editor for vts schemas. You draw your types on a canvas, wire them up, and the editor generates the .ts files for the schemas package described in the previous article. It ships as a small CLI (npx vtseditor) that runs inside your own project, reads and writes a single schema.json, and regenerates the TypeScript on every save.
There is no build step, no separate UI server to host, no database. Everything lives in your repository: the schema.json is the source of truth, the generated .ts files are derived artifacts, and both can be committed to git.
The left pane is the file tree (folders and files just like in your editor), the centre is the canvas where each box is a schema or an enum, and the lines between boxes are field references. Resize, drag, rearrange - the layout is persisted with the schema.
Getting Started
Install it as a dev dependency in the project where you want the schemas to live (in the previous article's setup, that would be inside schemas/):
npm install --save-dev vtseditor
Then drop a vtseditor.json next to your package.json:
{
"projects": [
{
"schemaPath": "./schemas/schema.json",
"destinationPath": "./schemas/src",
"destinationClear": false,
"autoGenerate": false,
"code": {
"schemaPrefix": "Schema",
"createTypes": true,
"createIndex": true,
"codeComment": true,
"codeIndent": " "
},
"scripts": {
"before_generate": [],
"after_generate": [
{ "path": "./schemas", "script": "npm run compile" }
]
}
}
],
"server": { "port": 5173 },
"browser": { "open": true }
}
The two important fields are:
-
schemaPath- where the editor stores its source-of-truth JSON file. -
destinationPath- where the generated.tsfiles end up. In the monorepo setup, this isschemas/src- exactly the folder thattsccompiles intoschemas/dist.
The after_generate hook then runs npm run compile inside the schemas package, so by the time the editor finishes saving, the backend and frontend already see the updated .d.ts.
Launch it with:
npx vtseditor
A browser tab opens at http://localhost:5173 and you are ready to draw.
Drawing a Schema
Click Add Schema, give it a name (User), and a new box appears on the canvas. Click any field row to open the field editor:
Each schema has:
- a name (this becomes the exported
UserSchema/Userpair), - an optional extend target (translates to
Vts.object({...}).extend(OtherSchema)in the output), - an ignoreAdditionalItems flag for the
objectSchemaoptions, - and an optional description, which ends up in the second argument of
Vts.object(...)and is later visible in API docs or error messages.
Fields are added inline. Each field has a name, a type, and modifiers like optional, array, unknown values. The type picker autocompletes against everything that currently exists in the project - primitive types, your own schemas, your enums, and even extern types from node_modules:
That autocomplete is more than a convenience - it is what keeps the canvas consistent. As soon as you rename User to Customer, every field that references it updates automatically, and the next save regenerates all dependent .ts files.
Linking Schemas Across Files
A real project has dozens of schemas in different folders. VTS Editor lets you point at any schema from any file - the editor handles the cross-file import for you when it generates the output:
In this screenshot, the orange box at the top is a link table - a visual reference to a schema that lives in a different file. The line connecting it to the schema below it represents an extend. When the file is generated, the editor inserts:
import {ContactCorporateSchema} from '../Contact/ContactCorporate.js';
export const ContactCorporateContactSchema = Vts.object({
// ...
}).extend(ContactCorporateSchema);
…without you having to remember the relative path. This alone has saved me hours over the last few months. Multi-file schemas in a hand-written schemas package are where I usually start making mistakes; with the editor, the imports are simply not my problem any more.
The AI Assistant
If you would rather describe the schema in words than draw it, every project has a small AI assistant baked in. Click Create Schema with AI, type a sentence, and a draft appears that you can refine before adding it to the canvas:
The screenshot above shows the assistant turning "Create a drink with percent" into an AlcoholicDrink schema with name, alcoholPercentage, volume and description. You can keep chatting ("add an ingredients array", "make description optional") and only press Generate once you are happy.
Five providers are supported out of the box: Anthropic, OpenAI, Gemini, LocalAI, and a Claude Code wrapper that uses the claude CLI you already have installed. Configure them in vtseditor.json - see doc/ConfigAI.md for the details.
To keep the conversation cheap on long sessions, past assistant turns are re-encoded in TOON (Token-Oriented Object Notation) before being replayed - that shaves roughly 25–30% off the replay cost compared to JSON.
Generated Output
Pressing the save button writes schema.json and runs the generator. For the User schema from earlier, the output ends up looking like this:
// auto-generated imports
import {ExtractSchemaResultType, Vts} from 'vts';
import {ProfileSchema} from './relative/path.js';
import {ExternalSchema} from 'external-package';
// enums ------------------------------------------------
export enum StatusEnum {
'active' = 'active',
'inactive' = 'inactive',
}
// schema + options ------------------------------------
export const UserSchema = Vts.object({
id: Vts.string({description: 'User identifier'}),
status: Vts.enum(StatusEnum),
profile: ProfileSchema,
}, {
description: 'User entity schema',
objectSchema: {
ignoreAdditionalItems: true
}
});
// matching TS type ------------------------------------
export type User = ExtractSchemaResultType<typeof UserSchema>;
That is exactly the kind of file the previous article's schemas package wants. Drop it into schemas/src/User/User.ts, re-export it from schemas/src/index.ts, and the backend and frontend pick it up via import { User } from 'myproject_schemas'.
For boundary checks the pattern stays the standard vts one:
const errors: SchemaErrors = [];
if (!UserSchema.validate(payload, errors)) {
throw new ValidationError(errors);
}
// payload is now typed as User
Integrating with the Monorepo
Hooking the editor into the monorepo from the previous article is a five-line job. Inside schemas/package.json:
{
"scripts": {
"compile": "tsc --project tsconfig.json",
"editor": "vtseditor"
},
"devDependencies": {
"vtseditor": "^1.2.0"
}
}
Now npm run editor --workspace=schemas opens the canvas, and the after_generate hook in vtseditor.json re-runs npm run compile after every save. The backend and frontend never even notice that anything special is happening - they just see new .d.ts files in schemas/dist.
Two more things worth knowing:
-
Extern schemas. Any package that ships its own
vtseditor.jsonand references a schema file via thevtseditorkey in itspackage.jsonshows up as a read-only project in the tree. You can reference its types from your own schemas, but you cannot edit them. Perfect for shared component libraries. -
Per-schema history. Since version 1.2.0, every save also captures a snapshot of the changed schemas and enums inside the chunk files. The editor's context menu has a View history entry that shows the diff and a one-click restore. Configurable via
editor.historySize(default 20).
Conclusion
The setup from the previous article gives you the structure of a monorepo with a shared schemas package. VTS Editor fills that package without forcing you to hand-write a single Vts.object(...). The schema.json becomes your source of truth, the generated .ts files are derived artifacts, and the whole thing fits inside the existing npm run compile pipeline.
If you want to try it out, the project is on GitHub: stefanwerfling/vtseditor - issues and PRs welcome.
The next article in this series will look at the MCP server built into the editor - how to let an AI agent like Claude or Cursor read and mutate the same schema.json over HTTP, in lockstep with an open browser tab.
Happy drawing.





Top comments (0)