On this page
- The Manual Client Code Problem
- The Code Generation Pipeline
- Unified Spec Parser: The Foundation
- Template System: Framework-Specific Code Generation
- Type Generation: TypeScript from JSON Schema
- Framework-Specific Patterns
- Request/Response Handling
- Operation ID Sanitization
- Code Organization: File Structure
- Configuration and Customization
- Error Handling in Generated Code
- The Bottom Line
🔗The Manual Client Code Problem
Every frontend team faces the same problem: you have an OpenAPI spec that defines your API, but you need to write client code to call it. You write the TypeScript types. You write the fetch calls. You write the error handling. You write the request/response transformation.
Then the API changes. You update the OpenAPI spec. But you forget to update the client code. Your types are wrong. Your tests fail. You spend hours debugging why your API calls are broken.
The problem isn't just the maintenance burden—it's that manually written client code is error-prone. You miss required fields. You get the parameter types wrong. You forget to handle edge cases. And every framework (React, Vue, Angular, Svelte) has different patterns, so you're writing and maintaining the same logic four times.
MockForge v0.2.0 solves this by generating framework-specific, type-safe API clients directly from OpenAPI specs. This post covers the technical implementation of how it works.
🔗The Code Generation Pipeline
The generation pipeline is a multi-stage process:
graph TD
A[OpenAPI Spec] -->|Parse| B[Unified Spec Parser]
B -->|Validate| C[Schema Analysis]
C -->|Extract| D[Operation Extraction]
D -->|Transform| E[Framework Template]
E -->|Generate| F[TypeScript Types]
E -->|Generate| G[Framework Code]
F -->|Bundle| H[Generated Client]
G -->|Bundle| H
The pipeline starts with parsing and validating the OpenAPI spec, then extracts operations and schemas, transforms them into framework-specific patterns, and generates the final code.
🔗Unified Spec Parser: The Foundation
The unified spec parser is where it all starts. It's not just parsing JSON/YAML—it's validating the spec, resolving $ref references, and extracting structured data for code generation:
pub struct UnifiedSpecParser;
impl UnifiedSpecParser {
pub fn parse(spec_path: &Path) -> Result<OpenApiSpec> {
// Read and parse spec file
let content = std::fs::read_to_string(spec_path)?;
let spec: Value = if spec_path.extension() == Some("yaml".as_ref()) {
serde_yaml::from_str(&content)?
} else {
serde_json::from_str(&content)?
};
// Validate OpenAPI version
let version = spec.get("openapi")
.or_else(|| spec.get("swagger"))
.ok_or_else(|| Error::InvalidSpec("Missing OpenAPI version"))?;
// Resolve $ref references
let resolved = Self::resolve_references(&spec)?;
// Extract structured data
let paths = Self::extract_paths(&resolved)?;
let schemas = Self::extract_schemas(&resolved)?;
let operations = Self::extract_operations(&paths)?;
Ok(OpenApiSpec {
version: version.as_str().unwrap().to_string(),
info: Self::extract_info(&resolved)?,
paths,
schemas,
operations,
})
}
fn resolve_references(spec: &Value) -> Result<Value> {
// Deep traversal to resolve all $ref pointers
// Handles both local references (#/components/schemas/User)
// and external references (./schemas/user.yaml#/User)
// ...
}
}
The parser handles:
- YAML and JSON formats
- OpenAPI 3.0 and 3.1
- Reference resolution (local and external)
- Schema validation
- Error reporting with file locations
This is critical: if the parser fails, you get clear error messages pointing to exactly what's wrong in your spec.
🔗Template System: Framework-Specific Code Generation
The template system is where framework-specific code is generated. Each framework (React, Vue, Angular, Svelte) has its own set of Handlebars templates:
pub struct ReactClientGenerator {
templates: Handlebars<'static>,
}
impl ReactClientGenerator {
fn register_templates(&mut self) -> Result<()> {
// TypeScript types template
self.templates.register_template_string("types", r#"
export interface {{name}} {
{{#each properties}}
{{@key}}{{#unless required}}?{{/unless}}: {{> typescript_type this}};
{{/each}}
}"#)?;
// React hook template
self.templates.register_template_string("hook", r#"
export function use{{operation_id}}({{parameters}}) {
return useQuery({
queryKey: ['{{operation_id}}', {{query_params}}],
queryFn: () => api.{{operation_id}}({{args}}),
});
}"#)?;
// API client template
self.templates.register_template_string("client", r#"
export class ApiClient {
{{#each operations}}
async {{operation_id}}({{params}}): Promise<{{response_type}}> {
const response = await fetch(`${this.baseURL}{{path}}`, {
method: '{{method}}',
headers: { ...this.headers, ...headers },
body: {{body}},
});
return response.json();
}
{{/each}}
}"#)?;
Ok(())
}
}
The templates use Handlebars for flexible code generation. They support:
- Conditionals (
{{#if}},{{#unless}}) - Iteration (
{{#each}}) - Partials (
{{> partial_name}}) - Helpers for custom logic
This makes it easy to generate framework-specific patterns while sharing common logic.
🔗Type Generation: TypeScript from JSON Schema
The type generation is where OpenAPI schemas become TypeScript types. The generator handles complex schema features:
fn generate_typescript_type(schema: &Schema) -> String {
match schema.type_.as_deref() {
Some("string") => {
if let Some(format) = &schema.format {
match format.as_str() {
"date" => "string",
"date-time" => "string",
"email" => "string",
"uuid" => "string",
_ => "string",
}
} else if let Some(enum_) = &schema.enum_ {
format!("{}", enum_.join(" | "))
} else {
"string"
}
}
Some("number") | Some("integer") => {
if schema.format.as_deref() == Some("int64") {
"number" // JavaScript numbers are 64-bit floats
} else {
"number"
}
}
Some("boolean") => "boolean",
Some("array") => {
if let Some(items) = &schema.items {
format!("{}[]", generate_typescript_type(items))
} else {
"any[]"
}
}
Some("object") => {
if let Some(properties) = &schema.properties {
let props: Vec<String> = properties
.iter()
.map(|(key, prop_schema)| {
let required = schema.required.as_ref()
.map(|r| r.contains(key))
.unwrap_or(false);
let type_ = generate_typescript_type(prop_schema);
format!(" {}:{};", key, if required { "" } else { "?" }, type_)
})
.collect();
format!("{{\n{}\n}}", props.join("\n"))
} else {
"{ [key: string]: any }"
}
}
_ => {
// Handle $ref, oneOf, anyOf, allOf
if let Some(ref_path) = &schema.ref_path {
resolve_ref_type(ref_path)
} else {
"any"
}
}
}
}
The type generator handles:
- Primitive types with formats (date, email, uuid)
- Arrays with item types
- Objects with properties and required fields
- Enums as union types
- References (
$ref) to component schemas - Complex schemas (oneOf, anyOf, allOf)
This ensures generated types are accurate and match your OpenAPI spec exactly.
🔗Framework-Specific Patterns
Each framework has different patterns. The generator adapts:
React:
// Generated React hook
export function useGetUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => api.getUser({ userId }),
});
}
// Usage in component
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useGetUser(userId);
// ...
}
Vue:
// Generated Vue composable
export function useGetUser(userId: Ref<string>) {
return useQuery({
queryKey: computed(() => ['user', userId.value]),
queryFn: () => api.getUser({ userId: userId.value }),
});
}
Angular:
// Generated Angular service
@Injectable({ providedIn: 'root' })
export class ApiService {
getUser(params: GetUserParams): Observable<User> {
return this.http.get<User>(`/users/${params.userId}`);
}
}
Svelte:
// Generated Svelte store
export function getUserStore(userId: string) {
return writable<User | null>(null);
}
The generator understands each framework's conventions and generates idiomatic code.
🔗Request/Response Handling
Generated clients handle requests and responses correctly:
class ApiClient {
async createUser(data: CreateUserRequest): Promise<User> {
// Validate required fields
if (!data.email) {
throw new RequiredError('email');
}
// Build request
const response = await fetch(`${this.baseURL}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
},
body: JSON.stringify(data),
});
// Handle errors
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new ApiError(
response.status,
response.statusText,
errorBody
);
}
// Parse and return response
return response.json();
}
}
The generated code includes:
- Required field validation
- Request body serialization
- Authentication headers
- Error handling with structured errors
- Response parsing
🔗Operation ID Sanitization
OpenAPI operation IDs aren't always valid JavaScript identifiers. The generator sanitizes them:
fn sanitize_identifier(operation_id: &str) -> String {
// Replace invalid characters
let sanitized = operation_id
.replace(" ", "_")
.replace("-", "_")
.replace("/", "_")
.replace("(", "")
.replace(")", "");
// Convert to camelCase
let parts: Vec<&str> = sanitized.split('_').collect();
let mut result = String::new();
for (i, part) in parts.iter().enumerate() {
if i == 0 {
result.push_str(&part.to_lowercase());
} else {
result.push_str(&capitalize(part));
}
}
result
}
This ensures generated method names are valid JavaScript identifiers while preserving the original operation ID's meaning.
🔗Code Organization: File Structure
Generated code is organized into a clear file structure:
generated/
├── types.ts # TypeScript types
├── client.ts # Base API client
├── hooks.ts # React hooks (if React)
├── composables.ts # Vue composables (if Vue)
├── service.ts # Angular service (if Angular)
├── stores.ts # Svelte stores (if Svelte)
├── errors.ts # Error classes
└── index.ts # Public API exports
This organization makes it easy to:
- Import only what you need
- Tree-shake unused code
- Maintain clear separation of concerns
- Extend with custom code
🔗Configuration and Customization
The generator supports extensive configuration:
generator:
framework: react
output:
directory: ./src/api
structure: flat # or 'nested'
naming:
operations: camelCase # or 'PascalCase', 'snake_case'
types: PascalCase
options:
includeHooks: true
includeTypes: true
includeUtils: true
filters:
includeOperations:
- getUser
- createUser
excludeOperations:
- deleteUser
This gives you control over:
- Output directory and file structure
- Naming conventions
- What gets generated
- Which operations to include/exclude
🔗Error Handling in Generated Code
Generated clients include structured error handling:
export class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
public body: any
) {
super(`API Error: ${status} ${statusText}`);
}
isClientError(): boolean {
return this.status >= 400 && this.status < 500;
}
isServerError(): boolean {
return this.status >= 500 && this.status < 600;
}
}
export class RequiredError extends Error {
constructor(public field: string) {
super(`Required parameter '${field}' was null or undefined`);
}
}
This makes error handling consistent and type-safe across your application.
🔗The Bottom Line
MockForge v0.2.0's client generation transforms API integration from manual, error-prone work into automated, type-safe code generation. The generated clients include:
- Complete TypeScript type definitions
- Framework-specific patterns (hooks, composables, services)
- Request/response validation
- Structured error handling
- Authentication support
- Configurable code organization
This is production-ready client code, generated automatically from your OpenAPI spec. No manual type definitions, no handwritten API calls, no framework-specific boilerplate. Just working, type-safe code that stays in sync with your API spec.
For teams building frontend applications, this feature eliminates weeks of manual client code and months of maintenance. That's the value of automated code generation.