You're integrating a third-party API. You open the docs, find a sample response, and start typing:
interface Product {
id: number;
name: string;
price: number;
// wait, is currency a string or an object?
// what's the shape of variants[]?
// does stock ever go null?
}
Twenty minutes later you have a fragile interface you're not fully confident in — and the moment the API ships a new field, it's already stale.
There's a better way.
snaptype: point it at the source, get types back
snaptype is a CLI that generates TypeScript interfaces and Zod schemas by reading your actual data — live API endpoints, OpenAPI specs, JSON files, CSV files.
No config. No code to write. One command.
from-url — types straight from a live endpoint
This is the one I use most. Give it any JSON endpoint:
npx snaptype from-url https://api.example.com/products --zod -o src/types/product.ts
snaptype fetches the response and generates:
export interface Product {
id: number;
name: string;
price: number;
currency: "USD" | "EUR" | "GBP";
stock: number | null;
createdAt: string;
variants: Variant[];
}
export interface Variant {
sku: string;
color: string;
size: "S" | "M" | "L" | "XL";
}
export const ProductSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number(),
currency: z.enum(["USD", "EUR", "GBP"]),
stock: z.number().nullable(),
createdAt: z.iso.datetime(),
variants: z.array(VariantSchema),
});
A few things worth noticing:
-
Nested objects are expanded —
variants[]became its ownVariantinterface automatically -
Enums are inferred —
currencyandsizehad a small set of repeated values, so snaptype made them union types instead ofstring -
Nullables are detected —
stockwasnullin some responses, so it becamenumber | null -
Semantic types —
createdAtbecamez.iso.datetime(), not a genericz.string()
This is inference against real data, not a naive field-by-field mapping.
Real-world example: GitHub API
Point it at any real endpoint — here the GitHub user API:
npx snaptype from-url https://api.github.com/users/torvalds --zod -o src/types/github-user.ts
Done. You now have typed access to every field in that response — including nested objects — with Zod schemas you can use directly for runtime validation.
When GitHub ships a new field, re-run the command. No manual diffing.
from-openapi — one file per schema, from any spec
If the API you're integrating has an OpenAPI or Swagger spec (most do), this is even better. Point it at the spec file:
npx snaptype from-openapi ./openapi.yaml -o src/types/
Or directly at the hosted spec URL:
npx snaptype from-openapi https://petstore3.swagger.io/api/v3/openapi.json -o src/types/
snaptype reads every schema definition in the spec and generates one typed file per schema. YAML and JSON both work.
For a spec that defines Pet, Order, and User, you get three files:
// src/types/Pet.ts
export interface Pet {
id?: number;
name: string;
photoUrls: string[];
status?: "available" | "pending" | "sold";
tags?: Tag[];
category?: Category;
}
// src/types/Order.ts
export interface Order {
id?: number;
petId?: number;
quantity?: number;
shipDate?: string;
status?: "placed" | "approved" | "delivered";
complete?: boolean;
}
Before, setting up a typed client from an OpenAPI spec meant:
- Finding (or writing) a code generator
- Configuring it
- Running it
- Cleaning up the generated mess
- Updating it every time the spec changes
Now it's one command.
Works with private and authenticated APIs
One-off request — pass headers directly with -H:
npx snaptype from-url https://api.example.com/me \
-H "Authorization: Bearer $TOKEN" \
--zod -o src/types/me.ts
Multiple endpoints on the same API — store the headers in .snaptyperc so you don't repeat yourself:
{
"baseUrl": "https://api.example.com",
"headers": {
"Authorization": "Bearer my-token",
"X-Tenant": "acme"
}
}
Then every from-url call picks them up automatically:
npx snaptype from-url /users -o src/types/user.ts
npx snaptype from-url /products -o src/types/product.ts
npx snaptype from-url /orders -o src/types/order.ts
Headers set in .snaptyperc are defaults — anything passed with -H on the CLI takes priority if the same key appears in both.
stdin fallback — if the endpoint requires a flow you can't replicate in a single request (custom auth, cookies, proxy), pipe the response in from curl:
curl -s -H "Authorization: Bearer $TOKEN" https://api.example.com/me \
| npx snaptype from-stdin --zod -o src/types/me.ts
JSON files and CSV too
If you're working from a local file rather than a live endpoint:
# Local JSON file
npx snaptype from-json ./user.json --zod -o src/types/user.ts
# CSV export from a database or spreadsheet
npx snaptype from-csv ./orders.csv --zod -o src/types/order.ts
The inference is the same — enums, nullables, semantic types, nested objects.
Zero config to start
No setup needed. If you want to pin defaults, add a .snaptyperc:
{
"zod": true,
"outDir": "src/types"
}
Then every command just picks up those defaults automatically.
Try it right now
Run it directly with npx — no install needed:
npx snaptype from-url https://api.github.com/users/torvalds --zod -o src/types/github-user.ts
Or add it as a dev dependency if you want a fixed version and faster runs:
npm install -D snaptype
snaptype from-url https://api.github.com/users/torvalds --zod -o src/types/github-user.ts
Full docs at snaptype.dev · Source on GitHub.
If it saves you time, a ⭐ on the repo goes a long way — it helps other developers find the tool.
Built this after one too many mornings spent re-typing interfaces from API docs. If from-url or from-openapi saves you time on a real project, drop a comment — I'm curious what APIs people are working with.
Feedback welcome
snaptype is still early-stage and I'm actively shaping what it becomes. If you hit a use case it doesn't handle, a command that behaves unexpectedly, or a feature you'd find genuinely useful — open an issue on GitHub or just leave a comment below. Every piece of feedback helps.
Top comments (0)