typescript-api-safety — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited typescript-api-safety (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The most dangerous data in any application is the data entering from the outside world. API responses, request bodies, environment variables, and file contents are all unknown at runtime — no matter what your TypeScript types say. The solution is a single guiding principle: parse, don't validate.
Validate checks if data matches a type and returns boolean. Parse transforms untyped data into a strictly-typed object at the boundary. After parsing, the data is trusted throughout the application.
// ❌ Validate (type assertion — lying to the compiler)
const user = response.json() as User; // No runtime check — crashes on malformed data
// ❌ Validate (boolean check — type widening, no transformation)
function isUser(data: unknown): boolean {
return typeof data === 'object' && data !== null;
}
// ...TypeScript still doesn't know data is User
// ✅ Parse (transform + prove type simultaneously)
const result = UserSchema.safeParse(await response.json());
if (!result.success) return sendError(400, result.error.flatten());
const user = result.data; // User — validated, typed, trustworthyZod schemas are the single source of truth for both runtime validation and TypeScript types. Never maintain separate interfaces and validators.
import { z } from 'zod';
// One schema — generates both the validator and the type
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'moderator']),
createdAt: z.coerce.date(), // Coerce string → Date automatically
metadata: z.record(z.string()).optional(),
});
// Derive TypeScript type from schema — never write the interface manually
type User = z.infer<typeof UserSchema>;
// Export schema for validation + type for usage
export { UserSchema, type User };.safeParse() at Boundaries.parse() throws on invalid input. .safeParse() returns a Result-like object. Always use .safeParse() at API boundaries.
// ❌ .parse() — throws ZodError, requires try/catch overhead
const user = UserSchema.parse(body); // Throws if invalid
// ✅ .safeParse() — returns Result, no throws
const result = UserSchema.safeParse(body);
if (!result.success) {
// result.error is ZodError with detailed validation failures
return {
status: 400,
errors: result.error.flatten().fieldErrors,
};
}
const user = result.data; // Fully typed UserValidate at every entry point into your system:
// Environment variables — validate at startup, crash fast if missing
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().int().min(1024).max(65535),
NODE_ENV: z.enum(['development', 'production', 'test']),
JWT_SECRET: z.string().min(32),
API_KEY: z.string().min(16),
});
const env = EnvSchema.parse(process.env); // Throw at startup if misconfigured
export type Env = typeof env;
export { env }; // env is fully typed throughout the appFastify's type provider architecture automatically synchronizes JSON Schema and TypeScript types:
import Fastify from 'fastify';
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from 'fastify-type-provider-zod';
const app = Fastify().withTypeProvider<ZodTypeProvider>();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
// Route types are inferred from schemas — no manual generics
app.post(
'/users',
{
schema: {
body: z.object({
email: z.string().email(),
name: z.string().min(1),
role: z.enum(['admin', 'user']),
}),
response: {
201: z.object({
id: z.string().uuid(),
email: z.string(),
name: z.string(),
}),
400: z.object({ error: z.string() }),
},
},
},
async (request, reply) => {
// request.body is fully typed: { email: string; name: string; role: 'admin' | 'user' }
const user = await createUser(request.body);
return reply.status(201).send(user); // Validated against response schema
}
);Never trust external API responses, even when the API has TypeScript SDKs.
const GithubUserSchema = z.object({
login: z.string(),
id: z.number(),
avatar_url: z.string().url(),
name: z.string().nullable(),
email: z.string().email().nullable(),
public_repos: z.number().int().nonnegative(),
});
type GithubUser = z.infer<typeof GithubUserSchema>;
async function fetchGithubUser(username: string): Promise<Result<GithubUser, 'not-found' | 'api-error'>> {
const response = await fetch(`https://api.github.com/users/${username}`);
if (response.status === 404) return err('not-found');
if (!response.ok) return err('api-error');
const json = await response.json();
const parsed = GithubUserSchema.safeParse(json);
if (!parsed.success) {
// API returned unexpected shape — treat as api-error
console.error('Github API schema drift:', parsed.error.flatten());
return err('api-error');
}
return ok(parsed.data);
}The universal contract pattern: share Zod schemas across packages to maintain a single source of truth.
packages/
@org/shared/
src/
schemas/
user.schema.ts ← Zod schema + z.infer type
order.schema.ts
index.ts ← Re-exports schemas and types
api/ ← imports schema for validation
web/ ← imports schema for form validation + type safety// @org/shared/src/schemas/user.schema.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
role: z.enum(['admin', 'user']),
});
export const UserSchema = CreateUserSchema.extend({
id: z.string().uuid(),
createdAt: z.coerce.date(),
});
export type CreateUser = z.infer<typeof CreateUserSchema>;
export type User = z.infer<typeof UserSchema>;
// API (backend):
import { CreateUserSchema } from '@org/shared';
app.post('/users', { schema: { body: CreateUserSchema } }, handler);
// Web (frontend):
import { CreateUserSchema, type CreateUser } from '@org/shared';
const form = useForm<CreateUser>({ resolver: zodResolver(CreateUserSchema) });Always use import type for type-only imports to prevent bundler issues:
// ✅ Correct — type is erased at compile time
import type { User } from '@org/shared';
import { UserSchema } from '@org/shared'; // Runtime value — kept
// ✅ Inline form
import { UserSchema, type User } from '@org/shared';For complete implementation guides, consult:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.