OpenAPI Best Practices for SpecJet
This guide covers best practices for designing OpenAPI contracts that work seamlessly with SpecJet’s TypeScript generation and mock server features.
Contract Design Principles
1. Schema-First Development
Design your API contract before writing any code:
# ✅ Good: Start with clear, complete contract
openapi: 3.0.0
info:
  title: User Management API
  version: 1.0.0
  description: Complete API for user management with clear schemas
components:
  schemas:
    User:
      type: object
      properties:
        id: { type: integer, example: 123 }
        name: { type: string, example: "John Doe" }
        email: { type: string, format: email, example: "john@example.com" }
      required: [id, name, email]
2. Consistent Naming Conventions
Use consistent, descriptive names throughout your contract:
# ✅ Good: Consistent naming
paths:
  /users:           # Plural resource names
    get:
      operationId: getUsers      # Verb + Resource
    post:
      operationId: createUser    # Verb + Singular
  
  /users/{userId}:  # Clear parameter names
    get:
      operationId: getUserById
    put:
      operationId: updateUser
    delete:
      operationId: deleteUser
# ❌ Bad: Inconsistent naming
paths:
  /user:            # Inconsistent plural/singular
    get:
      operationId: list_users    # Snake case mixed with camelCase
    post:
      operationId: add         # Vague operation name
3. Complete Schema Definitions
Provide complete, detailed schemas with examples:
# ✅ Good: Complete schema with validation
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 123
          description: Unique user identifier
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: "John Doe"
          description: User's full name
        email:
          type: string
          format: email
          example: "john@example.com"
          description: User's email address
        isActive:
          type: boolean
          default: true
          example: true
          description: Whether user account is active
        createdAt:
          type: string
          format: date-time
          example: "2023-01-01T12:00:00Z"
          description: Account creation timestamp
      required: [id, name, email]
      additionalProperties: false
# ❌ Bad: Minimal schema
components:
  schemas:
    User:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        email: { type: string }
TypeScript-Friendly Patterns
1. Clear Type Mappings
Design schemas that map cleanly to TypeScript:
# ✅ Good: Clear type mapping
UserStatus:
  type: string
  enum: [active, inactive, pending, suspended]
  example: active
UserRole:
  type: string
  enum: [admin, user, moderator]
  example: user
# Generates clean TypeScript:
# type UserStatus = 'active' | 'inactive' | 'pending' | 'suspended';
# type UserRole = 'admin' | 'user' | 'moderator';
2. Proper Optional Fields
Use required array correctly for optional vs required fields:
# ✅ Good: Clear required/optional distinction
CreateUserRequest:
  type: object
  properties:
    name: { type: string }           # Required
    email: { type: string }          # Required  
    bio: { type: string }            # Optional
    avatar: { type: string }         # Optional
    isActive: { type: boolean, default: true }  # Optional with default
  required: [name, email]           # Only truly required fields
# Generates:
# interface CreateUserRequest {
#   name: string;
#   email: string;
#   bio?: string;
#   avatar?: string;
#   isActive?: boolean;
# }
3. Avoid Complex Schema Patterns
Keep schemas simple for better TypeScript generation:
# ✅ Good: Simple, clear schemas
Address:
  type: object
  properties:
    street: { type: string }
    city: { type: string }
    country: { type: string }
    postalCode: { type: string }
User:
  type: object
  properties:
    id: { type: integer }
    name: { type: string }
    address: { $ref: '#/components/schemas/Address' }
# ❌ Avoid: Complex inheritance patterns
User:
  allOf:
    - $ref: '#/components/schemas/BaseEntity'
    - $ref: '#/components/schemas/TimestampMixin'
    - type: object
      properties:
        name: { type: string }
Mock-Server Friendly Design
1. Realistic Examples
Provide realistic examples for better mock data:
# ✅ Good: Realistic examples
User:
  type: object
  properties:
    id: { type: integer, example: 123 }
    name: { type: string, example: "Sarah Johnson" }
    email: { type: string, example: "sarah.johnson@company.com" }
    department: { type: string, example: "Engineering" }
    salary: { type: number, example: 75000 }
    startDate: { type: string, format: date, example: "2023-01-15" }
# ❌ Bad: Generic examples
User:
  type: object
  properties:
    id: { type: integer, example: 1 }
    name: { type: string, example: "string" }
    email: { type: string, example: "user@example.com" }
2. Proper Error Responses
Define comprehensive error responses:
# ✅ Good: Complete error handling
paths:
  /users/{userId}:
    get:
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '400':
          description: Invalid user ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Error:
      type: object
      properties:
        code: { type: string, example: "USER_NOT_FOUND" }
        message: { type: string, example: "User with ID 123 not found" }
        timestamp: { type: string, format: date-time }
      required: [code, message, timestamp]
    ValidationError:
      type: object
      properties:
        code: { type: string, example: "VALIDATION_ERROR" }
        message: { type: string, example: "Invalid input data" }
        errors: 
          type: array
          items:
            type: object
            properties:
              field: { type: string, example: "email" }
              message: { type: string, example: "Invalid email format" }
3. Pagination Patterns
Use consistent pagination across endpoints:
# ✅ Good: Consistent pagination
paths:
  /users:
    get:
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
            example: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
            example: 20
      responses:
        '200':
          description: Paginated list of users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedUsers'
components:
  schemas:
    PaginatedUsers:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/User'
        pagination:
          $ref: '#/components/schemas/PaginationInfo'
      required: [data, pagination]
    PaginationInfo:
      type: object
      properties:
        page: { type: integer, example: 1 }
        limit: { type: integer, example: 20 }
        total: { type: integer, example: 150 }
        pages: { type: integer, example: 8 }
        hasNext: { type: boolean, example: true }
        hasPrev: { type: boolean, example: false }
      required: [page, limit, total, pages, hasNext, hasPrev]
API Design Patterns
1. RESTful Resource Design
Follow REST conventions for predictable URLs:
# ✅ Good: RESTful design
paths:
  # Collection operations
  /users:
    get:      # List users
      operationId: getUsers
    post:     # Create user
      operationId: createUser
  # Resource operations  
  /users/{userId}:
    get:      # Get user
      operationId: getUserById
    put:      # Update user (full replace)
      operationId: updateUser
    patch:    # Update user (partial)
      operationId: patchUser
    delete:   # Delete user
      operationId: deleteUser
  # Sub-resource operations
  /users/{userId}/posts:
    get:      # Get user's posts
      operationId: getUserPosts
    post:     # Create post for user
      operationId: createUserPost
  /users/{userId}/posts/{postId}:
    get:      # Get specific user post
      operationId: getUserPost
    put:      # Update user post
      operationId: updateUserPost
    delete:   # Delete user post
      operationId: deleteUserPost
2. Consistent Request/Response Patterns
Use consistent patterns for similar operations:
# ✅ Good: Consistent create/update patterns
components:
  schemas:
    # Base entity with common fields
    User:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        email: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    # Create request (no ID, timestamps)
    CreateUserRequest:
      type: object
      properties:
        name: { type: string }
        email: { type: string }
      required: [name, email]
    # Update request (partial fields, no ID/timestamps)
    UpdateUserRequest:
      type: object
      properties:
        name: { type: string }
        email: { type: string }
      # All fields optional for PATCH-style updates
3. Authentication Patterns
Define clear authentication schemes:
# ✅ Good: Clear authentication
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token for authenticated requests
    
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key for service-to-service communication
# Apply security to operations
paths:
  /users:
    get:
      security:
        - BearerAuth: []     # Requires JWT token
    post:
      security:
        - BearerAuth: []
        
  /internal/stats:
    get:
      security:
        - ApiKeyAuth: []     # Requires API key
SpecJet-Specific Best Practices
1. Operation IDs for Method Names
Use clear operationId values for generated method names:
# ✅ Good: Clear operation IDs
paths:
  /users:
    get:
      operationId: getUsers        # api.getUsers()
    post:
      operationId: createUser      # api.createUser()
      
  /users/{userId}:
    get:
      operationId: getUserById     # api.getUserById(id)
    put:
      operationId: updateUser      # api.updateUser(id, data)
    delete:
      operationId: deleteUser      # api.deleteUser(id)
  /users/{userId}/avatar:
    post:
      operationId: uploadUserAvatar  # api.uploadUserAvatar(id, file)
# ❌ Bad: Missing or unclear operation IDs
paths:
  /users:
    get:      # No operationId - generates generic name
    post:
      operationId: create    # Too generic
      
  /users/{userId}:
    get:
      operationId: get_user_by_id  # Snake case doesn't match JS conventions
2. Parameter Naming
Use descriptive parameter names that become clear variable names:
# ✅ Good: Clear parameter names
paths:
  /users/{userId}:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: integer
          example: 123
    get:
      parameters:
        - name: includeProfile
          in: query
          schema:
            type: boolean
            default: false
        - name: expand
          in: query
          schema:
            type: array
            items:
              type: string
              enum: [posts, comments, followers]
# Generates clean method signature:
# getUserById(userId: number, options?: { includeProfile?: boolean, expand?: string[] })
# ❌ Bad: Generic parameter names
paths:
  /users/{id}:     # Too generic
    parameters:
      - name: id   # Could be any kind of ID
    get:
      parameters:
        - name: flag   # Unclear purpose
        - name: opts   # Too generic
3. Response Schema Organization
Organize schemas for reusability and clarity:
# ✅ Good: Organized, reusable schemas
components:
  schemas:
    # Base entities
    User:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        email: { type: string }
        profile: { $ref: '#/components/schemas/UserProfile' }
    UserProfile:
      type: object
      properties:
        bio: { type: string }
        avatar: { type: string }
        location: { type: string }
    # Request/response wrappers
    UserListResponse:
      type: object
      properties:
        users: 
          type: array
          items: { $ref: '#/components/schemas/User' }
        pagination: { $ref: '#/components/schemas/PaginationInfo' }
    CreateUserResponse:
      type: object
      properties:
        user: { $ref: '#/components/schemas/User' }
        message: { type: string, example: "User created successfully" }
    # Common patterns
    PaginationInfo:
      type: object
      properties:
        page: { type: integer }
        limit: { type: integer }
        total: { type: integer }
        hasNext: { type: boolean }
        hasPrev: { type: boolean }
    Error:
      type: object
      properties:
        code: { type: string }
        message: { type: string }
        timestamp: { type: string, format: date-time }
Core Workflow Validation
1. Contract Validation
Ensure your contract works with the core SpecJet workflow:
# Validate contract syntax
specjet generate --dry-run
# Test with mock server
specjet mock --scenario demo
# Generate documentation
specjet docs
2. Advanced: API Implementation Validation
⚠️ Advanced Feature: Only use after mastering the core workflow (init → generate → mock → docs)
Once your backend is implemented, validate it matches your contract:
# Validate against real API
specjet validate http://localhost:8000
3. Mock Data Quality
Design schemas that generate realistic mock data:
# ✅ Good: Realistic constraints for mock data
User:
  type: object
  properties:
    name:
      type: string
      minLength: 2
      maxLength: 50
      pattern: '^[A-Za-z\s]+$'
      example: "Sarah Johnson"
    email:
      type: string
      format: email
      example: "sarah.johnson@company.com"
    age:
      type: integer
      minimum: 18
      maximum: 100
      example: 29
    salary:
      type: number
      minimum: 30000
      maximum: 200000
      multipleOf: 1000
      example: 75000
# ❌ Bad: No constraints lead to unrealistic data
User:
  type: object
  properties:
    name: { type: string }      # Could generate random strings
    email: { type: string }     # Won't be valid email
    age: { type: integer }      # Could be negative or huge
    salary: { type: number }    # Could be unrealistic
5. Comprehensive Examples
Provide examples for different scenarios:
# ✅ Good: Multiple realistic examples
User:
  type: object
  properties:
    id: { type: integer }
    name: { type: string }
    role: { type: string, enum: [admin, user, moderator] }
  examples:
    admin:
      value:
        id: 1
        name: "Admin User"
        role: admin
    regular_user:
      value:
        id: 2
        name: "John Doe"
        role: user
    moderator:
      value:
        id: 3
        name: "Jane Smith"
        role: moderator
Common Anti-Patterns to Avoid
1. Overly Complex Schemas
# ❌ Bad: Overly complex schema
User:
  anyOf:
    - allOf:
        - $ref: '#/components/schemas/BaseUser'
        - oneOf:
            - $ref: '#/components/schemas/AdminMixin'
            - $ref: '#/components/schemas/UserMixin'
    - $ref: '#/components/schemas/GuestUser'
# ✅ Good: Simple, clear schema
User:
  type: object
  properties:
    id: { type: integer }
    name: { type: string }
    role: { type: string, enum: [admin, user, guest] }
  required: [id, name, role]
2. Inconsistent Data Types
# ❌ Bad: Inconsistent ID types
User:
  properties:
    id: { type: string }      # String ID
Post:
  properties:
    id: { type: integer }     # Integer ID
    userId: { type: string }  # References User.id
# ✅ Good: Consistent ID types
User:
  properties:
    id: { type: integer }
Post:
  properties:
    id: { type: integer }
    userId: { type: integer } # Matches User.id type
3. Missing Required Fields
# ❌ Bad: Unclear what's required
User:
  type: object
  properties:
    id: { type: integer }
    name: { type: string }
    email: { type: string }
  # Missing required array - everything appears optional
# ✅ Good: Clear required fields
User:
  type: object
  properties:
    id: { type: integer }
    name: { type: string }
    email: { type: string }
  required: [id, name, email]
Performance Considerations
1. Schema Size
Keep schemas reasonably sized for fast generation:
# ✅ Good: Focused schemas
User:
  type: object
  properties:
    # Core user properties (5-15 fields)
    id: { type: integer }
    name: { type: string }
    email: { type: string }
    # ... reasonable number of fields
# ❌ Bad: Massive schemas
User:
  type: object
  properties:
    # 50+ fields make generation slow and types unwieldy
2. Reference Usage
Use $ref for reusable schemas:
# ✅ Good: Reusable references
components:
  schemas:
    Address:
      type: object
      properties:
        street: { type: string }
        city: { type: string }
    User:
      properties:
        homeAddress: { $ref: '#/components/schemas/Address' }
        workAddress: { $ref: '#/components/schemas/Address' }
# ❌ Bad: Duplicated schemas
User:
  properties:
    homeAddress:
      type: object
      properties:
        street: { type: string }
        city: { type: string }
    workAddress:
      type: object
      properties:
        street: { type: string }  # Duplicated
        city: { type: string }    # Duplicated
Team Collaboration
1. Documentation
Document your design decisions:
openapi: 3.0.0
info:
  title: User Management API
  version: 1.0.0
  description: |
    Complete API for user management.
    
    ## Design Decisions
    - User IDs are integers for performance
    - All timestamps use ISO 8601 format
    - Pagination uses page/limit pattern
    - Errors follow RFC 7807 problem details
    
    ## Authentication
    Use Bearer tokens for all authenticated endpoints.
    
    ## Rate Limiting
    All endpoints are rate limited to 1000 requests/hour per user.
paths:
  /users:
    get:
      summary: List users
      description: |
        Returns paginated list of users.
        
        Supports filtering by:
        - role (admin, user, moderator)  
        - active status
        - creation date range
2. Versioning Strategy
Plan for API evolution:
openapi: 3.0.0
info:
  title: User Management API
  version: 2.0.0
  description: |
    Version 2.0 introduces:
    - New user profile fields
    - Enhanced filtering options
    - Improved error responses
    
    Breaking changes from v1:
    - User.username field removed (use User.email)
    - Error response format changed
servers:
  - url: https://api.myapp.com/v2
    description: Version 2.0 (current)
  - url: https://api.myapp.com/v1
    description: Version 1.0 (deprecated)
3. Change Management
Use examples to document expected behavior:
# Document expected behavior with examples
/users/{userId}:
  put:
    description: |
      Update user information.
      
      **Important**: This is a full replacement operation.
      All fields must be provided or they will be set to null/default.
      
      For partial updates, use PATCH /users/{userId} instead.
    examples:
      full_update:
        summary: Full user update
        value:
          name: "John Smith"
          email: "john.smith@company.com"
          isActive: true
Conclusion
Following these best practices ensures that your OpenAPI contracts work seamlessly with SpecJet’s core features. The key principles are:
Core Workflow Focus
- Master the basics first: init → generate → mock → docs
 - Consistency: Use consistent patterns throughout your API
 - Clarity: Make schemas and operations self-documenting
 - Reusability: Design for code reuse and maintainability
 - Realism: Provide realistic examples and constraints for mock data
 
Advanced Features
- Evolution: Plan for API changes and versioning
 - Validation: Use 
specjet validateonce backend is implemented 
For more specific guidance, see:
- Getting Started: Master the core workflow first
 - Commands Reference: Learn all SpecJet commands
 - Configuration Guide: Customize SpecJet for your patterns
 - Integration Guides: Framework-specific patterns
 - Troubleshooting: Solve common issues