DEV Community

Stefan Werfling
Stefan Werfling

Posted on

TypeScript - Drawing your Schemas with VTS Editor

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>;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

The two important fields are:

  • schemaPath - where the editor stores its source-of-truth JSON file.
  • destinationPath - where the generated .ts files end up. In the monorepo setup, this is schemas/src - exactly the folder that tsc compiles into schemas/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
Enter fullscreen mode Exit fullscreen mode

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 / User pair),
  • an optional extend target (translates to Vts.object({...}).extend(OtherSchema) in the output),
  • an ignoreAdditionalItems flag for the objectSchema options,
  • 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);
Enter fullscreen mode Exit fullscreen mode

…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>;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.json and references a schema file via the vtseditor key in its package.json shows 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)