FoundryKit

Integration Guide

Integrate FoundryKit types with popular frameworks and libraries

Integration Guide

Learn how to integrate FoundryKit types with popular frameworks, libraries, and tools to enhance type safety across your entire development stack.

React Integration

Component Props with Types

Seamlessly integrate FoundryKit types with React components.

import React, { forwardRef } from 'react'
import type {
  ComponentProps,
  ComponentRef,
  ForwardRefComponent,
  WithChildren,
  WithClassName,
  EventHandler,
  Optional
} from '@foundrykit/types'

// Base component with FoundryKit types
interface BaseButtonProps extends WithClassName {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
  onClick?: EventHandler<React.MouseEvent>
}

// Forward ref component with full type safety
const Button: ForwardRefComponent<'button', BaseButtonProps> = forwardRef(
  ({ variant = 'primary', size = 'md', disabled, loading, className, children, ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={cn(
          'btn',
          `btn-${variant}`,
          `btn-${size}`,
          { 'btn-disabled': disabled || loading },
          className
        )}
        disabled={disabled || loading}
        {...props}
      >
        {loading ? 'Loading...' : children}
      </button>
    )
  }
)

// Optional props pattern
interface UserCardProps extends WithChildren, WithClassName {
  user: Optional<User, 'avatar' | 'bio'>
  onEdit?: EventHandler<React.MouseEvent>
  showActions?: boolean
}

function UserCard({ user, onEdit, showActions = true, className, children }: UserCardProps) {
  return (
    <div className={cn('user-card', className)}>
      <div className="user-info">
        {user.avatar && <img src={user.avatar} alt="Avatar" />}
        <div>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          {user.bio && <p className="bio">{user.bio}</p>}
        </div>
      </div>
      {showActions && (
        <div className="actions">
          <button onClick={onEdit}>Edit</button>
        </div>
      )}
      {children}
    </div>
  )
}

Custom Hooks with Types

Create type-safe custom hooks.

import { useState, useCallback, useEffect } from 'react'
import type {
  ApiResponse,
  ApiError,
  PaginatedResponse,
  FormData,
  ValidationResult
} from '@foundrykit/types'

// Generic data fetching hook
interface UseApiOptions<T> {
  enabled?: boolean
  onSuccess?: (data: T) => void
  onError?: (error: ApiError) => void
}

interface UseApiResult<T> {
  data: T | null
  loading: boolean
  error: ApiError | null
  refetch: () => Promise<void>
}

function useApi<T>(
  fetcher: () => Promise<ApiResponse<T>>,
  options: UseApiOptions<T> = {}
): UseApiResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<ApiError | null>(null)

  const fetchData = useCallback(async () => {
    if (options.enabled === false) return

    setLoading(true)
    setError(null)

    try {
      const response = await fetcher()
      setData(response.data)
      options.onSuccess?.(response.data)
    } catch (err) {
      const apiError = err as ApiError
      setError(apiError)
      options.onError?.(apiError)
    } finally {
      setLoading(false)
    }
  }, [fetcher, options])

  useEffect(() => {
    fetchData()
  }, [fetchData])

  return { data, loading, error, refetch: fetchData }
}

// Form hook with validation
interface UseFormOptions<T> {
  initialData?: Partial<T>
  validate?: (data: T) => ValidationResult<T>
  onSubmit?: (data: T) => Promise<void>
}

interface UseFormResult<T> {
  data: T
  errors: Record<keyof T, string>
  isValid: boolean
  isSubmitting: boolean
  updateField: <K extends keyof T>(field: K, value: T[K]) => void
  handleSubmit: () => Promise<void>
  reset: () => void
}

function useForm<T extends Record<string, any>>(
  defaultData: T,
  options: UseFormOptions<T> = {}
): UseFormResult<T> {
  const [data, setData] = useState<T>({ ...defaultData, ...options.initialData })
  const [errors, setErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>)
  const [isSubmitting, setIsSubmitting] = useState(false)

  const updateField = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
    setData(prev => ({ ...prev, [field]: value }))
    // Clear error when field is updated
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: '' }))
    }
  }, [errors])

  const handleSubmit = useCallback(async () => {
    if (options.validate) {
      const validation = options.validate(data)
      if (!validation.isValid) {
        setErrors(validation.errors as Record<keyof T, string>)
        return
      }
    }

    if (options.onSubmit) {
      setIsSubmitting(true)
      try {
        await options.onSubmit(data)
      } catch (error) {
        console.error('Form submission error:', error)
      } finally {
        setIsSubmitting(false)
      }
    }
  }, [data, options])

  const reset = useCallback(() => {
    setData({ ...defaultData, ...options.initialData })
    setErrors({} as Record<keyof T, string>)
    setIsSubmitting(false)
  }, [defaultData, options.initialData])

  const isValid = Object.values(errors).every(error => !error)

  return {
    data,
    errors,
    isValid,
    isSubmitting,
    updateField,
    handleSubmit,
    reset
  }
}

// Usage example
interface UserFormData {
  name: string
  email: string
  age: number
}

function UserForm() {
  const {
    data,
    errors,
    isValid,
    isSubmitting,
    updateField,
    handleSubmit,
    reset
  } = useForm<UserFormData>(
    { name: '', email: '', age: 0 },
    {
      validate: (data) => {
        const errors: Partial<Record<keyof UserFormData, string>> = {}
        
        if (!data.name) errors.name = 'Name is required'
        if (!data.email) errors.email = 'Email is required'
        if (data.age < 18) errors.age = 'Must be 18 or older'
        
        return {
          isValid: Object.keys(errors).length === 0,
          errors: errors as Record<keyof UserFormData, string>
        }
      },
      onSubmit: async (data) => {
        console.log('Submitting:', data)
        // API call here
      }
    }
  )

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
      <input
        value={data.name}
        onChange={(e) => updateField('name', e.target.value)}
        placeholder="Name"
      />
      {errors.name && <span className="error">{errors.name}</span>}
      
      <input
        value={data.email}
        onChange={(e) => updateField('email', e.target.value)}
        placeholder="Email"
      />
      {errors.email && <span className="error">{errors.email}</span>}
      
      <input
        type="number"
        value={data.age}
        onChange={(e) => updateField('age', parseInt(e.target.value))}
        placeholder="Age"
      />
      {errors.age && <span className="error">{errors.age}</span>}
      
      <button type="submit" disabled={!isValid || isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
      
      <button type="button" onClick={reset}>
        Reset
      </button>
    </form>
  )
}

Next.js Integration

API Routes with Types

Type-safe Next.js API routes.

import type { NextApiRequest, NextApiResponse } from 'next'
import type { ApiResponse, ApiError, PaginatedResponse } from '@foundrykit/types'

// Typed API handler
interface TypedNextApiRequest<T = any> extends Omit<NextApiRequest, 'body'> {
  body: T
}

type ApiHandler<TRequest = any, TResponse = any> = (
  req: TypedNextApiRequest<TRequest>,
  res: NextApiResponse<ApiResponse<TResponse> | ApiError>
) => Promise<void> | void

// Generic API response helper
function createApiResponse<T>(data: T, message?: string): ApiResponse<T> {
  return {
    data,
    success: true,
    message,
    timestamp: new Date().toISOString()
  }
}

function createApiError(code: string, message: string, details?: any): ApiError {
  return {
    error: true,
    code,
    message,
    details,
    timestamp: new Date().toISOString()
  }
}

// User API endpoints
interface CreateUserRequest {
  name: string
  email: string
  age: number
}

interface UpdateUserRequest {
  name?: string
  email?: string
  age?: number
}

// GET /api/users
const getUsersHandler: ApiHandler<never, PaginatedResponse<User>> = async (req, res) => {
  const { page = 1, limit = 10 } = req.query

  try {
    const users = await getUsersFromDatabase({
      page: Number(page),
      limit: Number(limit)
    })
    
    const response: PaginatedResponse<User> = {
      data: users.data,
      success: true,
      timestamp: new Date().toISOString(),
      pagination: {
        page: Number(page),
        limit: Number(limit),
        total: users.total,
        totalPages: Math.ceil(users.total / Number(limit)),
        hasNextPage: Number(page) * Number(limit) < users.total,
        hasPrevPage: Number(page) > 1
      }
    }
    
    res.status(200).json(response)
  } catch (error) {
    res.status(500).json(createApiError('INTERNAL_ERROR', 'Failed to fetch users'))
  }
}

// POST /api/users
const createUserHandler: ApiHandler<CreateUserRequest, User> = async (req, res) => {
  const { name, email, age } = req.body

  // Validation
  if (!name || !email || !age) {
    return res.status(400).json(
      createApiError('VALIDATION_ERROR', 'Missing required fields', {
        required: ['name', 'email', 'age']
      })
    )
  }

  try {
    const user = await createUserInDatabase({ name, email, age })
    res.status(201).json(createApiResponse(user, 'User created successfully'))
  } catch (error) {
    res.status(500).json(createApiError('INTERNAL_ERROR', 'Failed to create user'))
  }
}

// Route handler with method switching
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  switch (req.method) {
    case 'GET':
      return getUsersHandler(req, res)
    case 'POST':
      return createUserHandler(req, res)
    default:
      res.setHeader('Allow', ['GET', 'POST'])
      res.status(405).json(createApiError('METHOD_NOT_ALLOWED', 'Method not allowed'))
  }
}

Type-safe Client-side Data Fetching

import type { ApiResponse, PaginatedResponse } from '@foundrykit/types'

// Type-safe API client for Next.js
class NextApiClient {
  private baseUrl: string

  constructor(baseUrl = '/api') {
    this.baseUrl = baseUrl
  }

  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    })

    if (!response.ok) {
      const error = await response.json()
      throw error
    }

    return response.json()
  }

  async getUsers(params?: {
    page?: number
    limit?: number
  }): Promise<PaginatedResponse<User>> {
    const searchParams = new URLSearchParams()
    if (params?.page) searchParams.set('page', String(params.page))
    if (params?.limit) searchParams.set('limit', String(params.limit))
    
    const query = searchParams.toString()
    return this.request<PaginatedResponse<User>>(
      `/users${query ? `?${query}` : ''}`
    )
  }

  async createUser(userData: CreateUserRequest): Promise<ApiResponse<User>> {
    return this.request<ApiResponse<User>>('/users', {
      method: 'POST',
      body: JSON.stringify(userData),
    })
  }

  async updateUser(id: string, userData: UpdateUserRequest): Promise<ApiResponse<User>> {
    return this.request<ApiResponse<User>>(`/users/${id}`, {
      method: 'PUT',
      body: JSON.stringify(userData),
    })
  }
}

// Usage in Next.js pages
import { GetServerSideProps } from 'next'

interface UsersPageProps {
  initialUsers: PaginatedResponse<User>
}

export default function UsersPage({ initialUsers }: UsersPageProps) {
  const [users, setUsers] = useState(initialUsers)
  const apiClient = new NextApiClient()

  const loadMore = async () => {
    const nextPage = await apiClient.getUsers({
      page: users.pagination.page + 1,
      limit: users.pagination.limit
    })
    
    setUsers(prev => ({
      ...nextPage,
      data: [...prev.data, ...nextPage.data]
    }))
  }

  return (
    <div>
      {users.data.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      
      {users.pagination.hasNextPage && (
        <button onClick={loadMore}>Load More</button>
      )}
    </div>
  )
}

export const getServerSideProps: GetServerSideProps<UsersPageProps> = async () => {
  const apiClient = new NextApiClient('http://localhost:3000/api')
  const initialUsers = await apiClient.getUsers({ page: 1, limit: 10 })

  return {
    props: {
      initialUsers
    }
  }
}

Express.js Integration

Typed Express Routes

Create type-safe Express.js routes.

import express from 'express'
import type { Request, Response, NextFunction } from 'express'
import type { ApiResponse, ApiError } from '@foundrykit/types'

// Typed Express request/response
interface TypedRequest<TBody = any, TQuery = any, TParams = any> extends Request {
  body: TBody
  query: TQuery
  params: TParams
}

interface TypedResponse<T = any> extends Response {
  json: (body: ApiResponse<T> | ApiError) => TypedResponse<T>
}

type TypedRequestHandler<TBody = any, TResponse = any, TQuery = any, TParams = any> = (
  req: TypedRequest<TBody, TQuery, TParams>,
  res: TypedResponse<TResponse>,
  next: NextFunction
) => void | Promise<void>

// Response helpers
function successResponse<T>(data: T, message?: string): ApiResponse<T> {
  return {
    data,
    success: true,
    message,
    timestamp: new Date().toISOString()
  }
}

function errorResponse(code: string, message: string, details?: any): ApiError {
  return {
    error: true,
    code,
    message,
    details,
    timestamp: new Date().toISOString()
  }
}

// User routes with full type safety
interface GetUsersQuery {
  page?: string
  limit?: string
  search?: string
}

interface GetUserParams {
  id: string
}

interface CreateUserBody {
  name: string
  email: string
  age: number
}

interface UpdateUserBody {
  name?: string
  email?: string
  age?: number
}

// GET /users
const getUsers: TypedRequestHandler<never, User[], GetUsersQuery> = async (req, res) => {
  try {
    const page = parseInt(req.query.page || '1')
    const limit = parseInt(req.query.limit || '10')
    const search = req.query.search

    const users = await getUsersFromDatabase({ page, limit, search })
    res.json(successResponse(users))
  } catch (error) {
    res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to fetch users'))
  }
}

// GET /users/:id
const getUser: TypedRequestHandler<never, User, never, GetUserParams> = async (req, res) => {
  try {
    const { id } = req.params
    const user = await getUserFromDatabase(id)
    
    if (!user) {
      return res.status(404).json(
        errorResponse('NOT_FOUND', 'User not found', { id })
      )
    }
    
    res.json(successResponse(user))
  } catch (error) {
    res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to fetch user'))
  }
}

// POST /users
const createUser: TypedRequestHandler<CreateUserBody, User> = async (req, res) => {
  try {
    const { name, email, age } = req.body
    
    // Validation
    if (!name || !email || !age) {
      return res.status(400).json(
        errorResponse('VALIDATION_ERROR', 'Missing required fields', {
          required: ['name', 'email', 'age']
        })
      )
    }
    
    const user = await createUserInDatabase({ name, email, age })
    res.status(201).json(successResponse(user, 'User created successfully'))
  } catch (error) {
    res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to create user'))
  }
}

// PUT /users/:id
const updateUser: TypedRequestHandler<UpdateUserBody, User, never, GetUserParams> = async (req, res) => {
  try {
    const { id } = req.params
    const updateData = req.body
    
    const user = await updateUserInDatabase(id, updateData)
    
    if (!user) {
      return res.status(404).json(
        errorResponse('NOT_FOUND', 'User not found', { id })
      )
    }
    
    res.json(successResponse(user, 'User updated successfully'))
  } catch (error) {
    res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to update user'))
  }
}

// Express router with typed routes
const userRouter = express.Router()

userRouter.get('/', getUsers)
userRouter.get('/:id', getUser)
userRouter.post('/', createUser)
userRouter.put('/:id', updateUser)

export default userRouter

GraphQL Integration

Type-safe GraphQL with FoundryKit Types

import { buildSchema, GraphQLResolveInfo } from 'graphql'
import type { ApiResponse, PaginatedResponse } from '@foundrykit/types'

// GraphQL context with types
interface GraphQLContext {
  user?: User
  apiClient: ApiClient
}

// Resolver types
type GraphQLResolver<TArgs = any, TResult = any> = (
  parent: any,
  args: TArgs,
  context: GraphQLContext,
  info: GraphQLResolveInfo
) => Promise<TResult> | TResult

// Query argument types
interface GetUsersArgs {
  page?: number
  limit?: number
  search?: string
}

interface GetUserArgs {
  id: string
}

interface CreateUserArgs {
  input: CreateUserInput
}

interface CreateUserInput {
  name: string
  email: string
  age: number
}

// GraphQL resolvers with full type safety
const resolvers = {
  Query: {
    users: (async (_, args: GetUsersArgs, context) => {
      const { page = 1, limit = 10, search } = args
      
      try {
        const response = await context.apiClient.getUsers({ page, limit, search })
        return {
          data: response.data,
          pagination: response.pagination
        }
      } catch (error) {
        throw new Error('Failed to fetch users')
      }
    }) as GraphQLResolver<GetUsersArgs, PaginatedResponse<User>>,

    user: (async (_, args: GetUserArgs, context) => {
      try {
        const response = await context.apiClient.getUser(args.id)
        return response.data
      } catch (error) {
        throw new Error('Failed to fetch user')
      }
    }) as GraphQLResolver<GetUserArgs, User>
  },

  Mutation: {
    createUser: (async (_, args: CreateUserArgs, context) => {
      try {
        const response = await context.apiClient.createUser(args.input)
        return response.data
      } catch (error) {
        throw new Error('Failed to create user')
      }
    }) as GraphQLResolver<CreateUserArgs, User>
  }
}

// GraphQL schema
const schema = buildSchema(`
  type User {
    id: ID!
    name: String!
    email: String!
    age: Int!
  }

  type Pagination {
    page: Int!
    limit: Int!
    total: Int!
    totalPages: Int!
    hasNextPage: Boolean!
    hasPrevPage: Boolean!
  }

  type UserConnection {
    data: [User!]!
    pagination: Pagination!
  }

  input CreateUserInput {
    name: String!
    email: String!
    age: Int!
  }

  type Query {
    users(page: Int, limit: Int, search: String): UserConnection!
    user(id: ID!): User
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
  }
`)

// Type-safe GraphQL client
interface GraphQLResponse<T> {
  data?: T
  errors?: Array<{ message: string; path?: string[] }>
}

class TypedGraphQLClient {
  constructor(private endpoint: string) {}

  private async request<T>(query: string, variables?: any): Promise<T> {
    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables })
    })

    const result: GraphQLResponse<T> = await response.json()

    if (result.errors) {
      throw new Error(result.errors[0].message)
    }

    return result.data!
  }

  async getUsers(variables: GetUsersArgs): Promise<{ users: PaginatedResponse<User> }> {
    const query = `
      query GetUsers($page: Int, $limit: Int, $search: String) {
        users(page: $page, limit: $limit, search: $search) {
          data {
            id
            name
            email
            age
          }
          pagination {
            page
            limit
            total
            totalPages
            hasNextPage
            hasPrevPage
          }
        }
      }
    `
    
    return this.request(query, variables)
  }

  async createUser(variables: CreateUserArgs): Promise<{ createUser: User }> {
    const query = `
      mutation CreateUser($input: CreateUserInput!) {
        createUser(input: $input) {
          id
          name
          email
          age
        }
      }
    `
    
    return this.request(query, variables)
  }
}

Database Integration (Prisma)

Type-safe Database Operations

import { PrismaClient } from '@prisma/client'
import type { ApiResponse, PaginatedResponse } from '@foundrykit/types'

const prisma = new PrismaClient()

// Repository pattern with FoundryKit types
interface UserRepository {
  findMany(params: {
    page: number
    limit: number
    search?: string
  }): Promise<PaginatedResponse<User>>
  
  findById(id: string): Promise<User | null>
  create(data: CreateUserRequest): Promise<User>
  update(id: string, data: UpdateUserRequest): Promise<User | null>
  delete(id: string): Promise<boolean>
}

class PrismaUserRepository implements UserRepository {
  async findMany({ page, limit, search }): Promise<PaginatedResponse<User>> {
    const skip = (page - 1) * limit
    
    const where = search
      ? {
          OR: [
            { name: { contains: search, mode: 'insensitive' as const } },
            { email: { contains: search, mode: 'insensitive' as const } }
          ]
        }
      : {}

    const [users, total] = await Promise.all([
      prisma.user.findMany({
        where,
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' }
      }),
      prisma.user.count({ where })
    ])

    return {
      data: users,
      success: true,
      timestamp: new Date().toISOString(),
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
        hasNextPage: page * limit < total,
        hasPrevPage: page > 1
      }
    }
  }

  async findById(id: string): Promise<User | null> {
    return prisma.user.findUnique({
      where: { id }
    })
  }

  async create(data: CreateUserRequest): Promise<User> {
    return prisma.user.create({
      data: {
        name: data.name,
        email: data.email,
        age: data.age
      }
    })
  }

  async update(id: string, data: UpdateUserRequest): Promise<User | null> {
    try {
      return await prisma.user.update({
        where: { id },
        data: {
          ...(data.name && { name: data.name }),
          ...(data.email && { email: data.email }),
          ...(data.age && { age: data.age })
        }
      })
    } catch (error) {
      return null
    }
  }

  async delete(id: string): Promise<boolean> {
    try {
      await prisma.user.delete({
        where: { id }
      })
      return true
    } catch (error) {
      return false
    }
  }
}

// Service layer with type safety
class UserService {
  constructor(private repository: UserRepository) {}

  async getUsers(params: {
    page?: number
    limit?: number
    search?: string
  }): Promise<PaginatedResponse<User>> {
    const { page = 1, limit = 10, search } = params
    return this.repository.findMany({ page, limit, search })
  }

  async getUser(id: string): Promise<ApiResponse<User | null>> {
    const user = await this.repository.findById(id)
    
    return {
      data: user,
      success: true,
      timestamp: new Date().toISOString(),
      message: user ? 'User found' : 'User not found'
    }
  }

  async createUser(data: CreateUserRequest): Promise<ApiResponse<User>> {
    const user = await this.repository.create(data)
    
    return {
      data: user,
      success: true,
      timestamp: new Date().toISOString(),
      message: 'User created successfully'
    }
  }

  async updateUser(id: string, data: UpdateUserRequest): Promise<ApiResponse<User | null>> {
    const user = await this.repository.update(id, data)
    
    return {
      data: user,
      success: true,
      timestamp: new Date().toISOString(),
      message: user ? 'User updated successfully' : 'User not found'
    }
  }

  async deleteUser(id: string): Promise<ApiResponse<boolean>> {
    const success = await this.repository.delete(id)
    
    return {
      data: success,
      success,
      timestamp: new Date().toISOString(),
      message: success ? 'User deleted successfully' : 'Failed to delete user'
    }
  }
}

// Usage
const userRepository = new PrismaUserRepository()
const userService = new UserService(userRepository)

export { userService }

Testing Integration

Type-safe Testing with Jest/Vitest

import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { ApiResponse, ApiError, ValidationResult } from '@foundrykit/types'

// Mock API responses with types
const createMockApiResponse = <T>(data: T): ApiResponse<T> => ({
  data,
  success: true,
  timestamp: new Date().toISOString()
})

const createMockApiError = (code: string, message: string): ApiError => ({
  error: true,
  code,
  message,
  timestamp: new Date().toISOString()
})

// Test utilities
interface TestUser {
  id: string
  name: string
  email: string
  age: number
}

const createTestUser = (overrides: Partial<TestUser> = {}): TestUser => ({
  id: '1',
  name: 'John Doe',
  email: 'john@example.com',
  age: 30,
  ...overrides
})

// Component testing with types
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { UserForm } from './UserForm'

describe('UserForm', () => {
  const mockOnSubmit = vi.fn<[TestUser], Promise<void>>()

  beforeEach(() => {
    mockOnSubmit.mockClear()
  })

  it('should submit form with valid data', async () => {
    const testUser = createTestUser()
    
    render(<UserForm onSubmit={mockOnSubmit} />)

    fireEvent.change(screen.getByLabelText(/name/i), {
      target: { value: testUser.name }
    })
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: testUser.email }
    })
    fireEvent.change(screen.getByLabelText(/age/i), {
      target: { value: String(testUser.age) }
    })

    fireEvent.click(screen.getByRole('button', { name: /submit/i }))

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        name: testUser.name,
        email: testUser.email,
        age: testUser.age
      })
    })
  })

  it('should show validation errors for invalid data', async () => {
    render(<UserForm onSubmit={mockOnSubmit} />)

    fireEvent.click(screen.getByRole('button', { name: /submit/i }))

    await waitFor(() => {
      expect(screen.getByText(/name is required/i)).toBeInTheDocument()
      expect(screen.getByText(/email is required/i)).toBeInTheDocument()
    })

    expect(mockOnSubmit).not.toHaveBeenCalled()
  })
})

// API testing with types
describe('UserAPI', () => {
  it('should fetch users successfully', async () => {
    const mockUsers = [createTestUser(), createTestUser({ id: '2', name: 'Jane Doe' })]
    const mockResponse = createMockApiResponse(mockUsers)

    global.fetch = vi.fn().mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve(mockResponse)
    })

    const apiClient = new UserApiClient()
    const result = await apiClient.getUsers()

    expect(result.data).toEqual(mockUsers)
    expect(result.success).toBe(true)
  })

  it('should handle API errors', async () => {
    const mockError = createMockApiError('NOT_FOUND', 'Users not found')

    global.fetch = vi.fn().mockResolvedValueOnce({
      ok: false,
      json: () => Promise.resolve(mockError)
    })

    const apiClient = new UserApiClient()

    await expect(apiClient.getUsers()).rejects.toEqual(mockError)
  })
})

// Validation testing
describe('UserValidation', () => {
  it('should validate user data correctly', () => {
    const validUser = createTestUser()
    const result = validateUser(validUser)

    expect(result.isValid).toBe(true)
    expect(Object.keys(result.errors)).toHaveLength(0)
  })

  it('should return errors for invalid user data', () => {
    const invalidUser = createTestUser({ name: '', email: 'invalid-email', age: 15 })
    const result = validateUser(invalidUser)

    expect(result.isValid).toBe(false)
    expect(result.errors.name).toBeTruthy()
    expect(result.errors.email).toBeTruthy()
    expect(result.errors.age).toBeTruthy()
  })
})

// Type guard testing
describe('Type Guards', () => {
  it('should correctly identify valid user objects', () => {
    const validUser = createTestUser()
    expect(isUser(validUser)).toBe(true)
  })

  it('should reject invalid user objects', () => {
    const invalidUser = { id: 1, name: 'John' } // missing email, age; wrong id type
    expect(isUser(invalidUser)).toBe(false)
  })
})

Next Steps