Skip to content

Add zod validation in more places #434

@ECWireless

Description

@ECWireless

Is your feature request related to a problem?

Many data fetches are not handling errors, nor validating data types.

Describe the solution you'd like

This issue identifies areas where Zod schemas can be used to validate:

  1. API endpoint inputs (query parameters, request bodies)
  2. API endpoint return values (response data)
  3. External API responses (third-party API data)

Current State

  • Zod is already installed (zod@^4.1.12 in package.json)
  • Zod is currently used in pages/api/usage.tsx to validate external API responses (DayDataSchema)
  • Basic address validation exists via isValidAddress() helper, but could be enhanced with Zod

1. API Endpoint Input Validation

1.1 Address-based Endpoints

These endpoints accept an address query parameter that should be validated:

/api/account-balance/[address].tsx

  • Input: address query param
  • Current validation: isValidAddress(address) - basic string check
  • Zod opportunity: Create AddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/) for stricter validation

/api/ens-data/[address].tsx

  • Input: address query param
  • Current validation: isValidAddress(address) + blacklist check
  • Zod opportunity: Same as above, plus validate blacklist separately

/api/score/[address].tsx

  • Input: address query param
  • Current validation: isValidAddress(address)
  • Zod opportunity: Address validation schema

/api/pending-stake/[address].tsx

  • Input: address query param
  • Current validation: isValidAddress(address)
  • Zod opportunity: Address validation schema

/api/l1-delegator/[address].tsx

  • Input: address query param
  • Current validation: isValidAddress(address)
  • Zod opportunity: Address validation schema

1.2 Treasury/Proposal Endpoints

/api/treasury/proposal/[proposalId]/state.tsx

  • Input: proposalId query param (string)
  • Current validation: Basic existence check if (!proposalId)
  • Zod opportunity:
    const ProposalIdSchema = z.string().regex(/^\d+$/) // numeric string

/api/treasury/proposal/[proposalId]/votes/[address].tsx

  • Input:
    • proposalId query param
    • address query param
  • Current validation: Basic checks
  • Zod opportunity: Combined schema for both params

/api/treasury/votes/[address]/index.tsx

  • Input: address query param
  • Current validation: isValidAddress(address)
  • Zod opportunity: Address validation schema

/api/treasury/votes/[address]/registered.tsx

  • Input: address query param
  • Current validation: isValidAddress(address)
  • Zod opportunity: Address validation schema

1.3 POST Endpoints with Request Bodies

/api/upload-ipfs.tsx

  • Input: req.body (JSON object)
  • Current validation: None - directly passes req.body to external API
  • Zod opportunity:
    const IpfsUploadSchema = z.object({
      // Define expected structure of IPFS upload data
      // This depends on what data is actually being uploaded
    })

/api/generateProof.tsx

  • Input: req.body with { account, delegate, stake, fees }
  • Current validation: None - directly destructures from req.body
  • Zod opportunity:
    const GenerateProofSchema = z.object({
      account: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
      delegate: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
      stake: z.string(), // or z.bigint() if needed
      fees: z.string(),  // or z.bigint() if needed
    })

1.4 Query Parameters with Optional Values

/api/pipelines/index.tsx

  • Input: Optional region query param
  • Current validation: None
  • Zod opportunity:
    const PipelinesQuerySchema = z.object({
      region: z.string().optional(),
    })

/api/score/index.tsx

  • Input: Optional pipeline and model query params
  • Current validation: None
  • Zod opportunity:
    const ScoreQuerySchema = z.object({
      pipeline: z.string().optional(),
      model: z.string().optional(),
    })

2. API Endpoint Return Value Validation

All API endpoints return typed data, but there's no runtime validation. Adding Zod schemas would catch contract/API changes early.

2.1 Account/Balance Endpoints

/api/account-balance/[address].tsx

  • Return type: AccountBalance
  • Zod opportunity:
    const AccountBalanceSchema = z.object({
      balance: z.string(),
      allowance: z.string(),
    })

/api/pending-stake/[address].tsx

  • Return type: PendingFeesAndStake
  • Zod opportunity:
    const PendingFeesAndStakeSchema = z.object({
      pendingStake: z.string(),
      pendingFees: z.string(),
    })

/api/l1-delegator/[address].tsx

  • Return type: L1Delegator
  • Zod opportunity:
    const UnbondingLockSchema = z.object({
      id: z.number(),
      amount: z.string(),
      withdrawRound: z.string(),
    })
    
    const L1DelegatorSchema = z.object({
      delegateAddress: z.string(),
      pendingStake: z.string(),
      pendingFees: z.string(),
      transcoderStatus: z.enum(["not-registered", "registered"]),
      unbondingLocks: z.array(UnbondingLockSchema),
      activeLocks: z.array(UnbondingLockSchema),
    })

2.2 ENS Endpoints

/api/ens-data/[address].tsx

  • Return type: EnsIdentity
  • Zod opportunity:
    const EnsIdentitySchema = z.object({
      id: z.string(),
      idShort: z.string(),
      avatar: z.string().nullable().optional(),
      name: z.string().nullable().optional(),
      url: z.string().nullable().optional(),
      twitter: z.string().nullable().optional(),
      github: z.string().nullable().optional(),
      description: z.string().nullable().optional(),
    })

2.3 Round/Protocol Endpoints

/api/current-round.tsx

  • Return type: CurrentRoundInfo
  • Zod opportunity:
    const CurrentRoundInfoSchema = z.object({
      id: z.number(),
      startBlock: z.number(),
      initialized: z.boolean(),
      currentL1Block: z.number(),
      currentL2Block: z.number(),
    })

2.4 Performance/Score Endpoints

/api/score/[address].tsx

  • Return type: PerformanceMetrics
  • Zod opportunity:
    const RegionalValuesSchema = z.record(z.string(), z.number())
    
    const ScoreSchema = z.object({
      value: z.number(),
      region: z.string(),
      model: z.string(),
      pipeline: z.string(),
      orchestrator: z.string(),
    })
    
    const PerformanceMetricsSchema = z.object({
      successRates: RegionalValuesSchema,
      roundTripScores: RegionalValuesSchema,
      scores: RegionalValuesSchema,
      pricePerPixel: z.number(),
      topAIScore: ScoreSchema,
    })

/api/score/index.tsx

  • Return type: AllPerformanceMetrics
  • Zod opportunity:
    const AllPerformanceMetricsSchema = z.record(
      z.string(),
      PerformanceMetricsSchema
    )

2.5 Treasury Endpoints

/api/treasury/proposal/[proposalId]/state.tsx

  • Return type: ProposalState
  • Zod opportunity:
    const ProposalStateSchema = z.object({
      id: z.string(),
      state: z.enum([
        "Pending",
        "Active",
        "Canceled",
        "Defeated",
        "Succeeded",
        "Queued",
        "Expired",
        "Executed",
        "Unknown",
      ]),
      quota: z.string(),
      quorum: z.string(),
      totalVoteSupply: z.string(),
      votes: z.object({
        against: z.string(),
        for: z.string(),
        abstain: z.string(),
      }),
    })

/api/treasury/votes/[address]/index.tsx

  • Return type: VotingPower
  • Zod opportunity:
    const VotingPowerSchema = z.object({
      proposalThreshold: z.string(),
      self: z.object({
        address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
        votes: z.string(),
      }),
      delegate: z.object({
        address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
        votes: z.string(),
      }).optional(),
    })

/api/treasury/votes/[address]/registered.tsx

  • Return type: RegisteredToVote
  • Zod opportunity:
    const RegisteredToVoteSchema = z.object({
      registered: z.boolean(),
      delegate: z.object({
        address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
        registered: z.boolean(),
      }),
    })

/api/treasury/proposal/[proposalId]/votes/[address].tsx

  • Return type: ProposalVotingPower
  • Zod opportunity:
    const ProposalVotingPowerSchema = z.object({
      self: z.object({
        address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
        votes: z.string(),
        hasVoted: z.boolean(),
      }),
      delegate: z.object({
        address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
        votes: z.string(),
        hasVoted: z.boolean(),
      }).optional(),
    })

2.6 Pipeline/Region Endpoints

/api/pipelines/index.tsx

  • Return type: AvailablePipelines
  • Zod opportunity:
    const PipelineSchema = z.object({
      id: z.string(),
      models: z.array(z.string()),
      regions: z.array(z.string()),
    })
    
    const AvailablePipelinesSchema = z.object({
      pipelines: z.array(PipelineSchema),
    })

/api/regions/index.ts

  • Return type: Regions
  • Zod opportunity:
    const RegionSchema = z.object({
      id: z.string(),
      name: z.string(),
      type: z.enum(["transcoding", "ai"]),
    })
    
    const RegionsSchema = z.object({
      regions: z.array(RegionSchema),
    })

2.7 Usage/Chart Endpoints

/api/usage.tsx

  • Return type: HomeChartData
  • Current validation: Already uses DayDataSchema for external API response
  • Zod opportunity: Create schema for the full HomeChartData return type

/api/upload-ipfs.tsx

  • Return type: AddIpfs
  • Zod opportunity:
    const AddIpfsSchema = z.object({
      hash: z.string(),
    })

3. External API Response Validation

These endpoints fetch data from external APIs and should validate responses before using them.

3.1 Metrics/AI Server Responses

/api/score/[address].tsx

  • External APIs:
    1. NEXT_PUBLIC_AI_METRICS_SERVER_URL/api/top_ai_scoreScoreResponse
    2. NEXT_PUBLIC_METRICS_SERVER_URL/api/aggregated_statsMetricsResponse
    3. Pricing URL → PriceResponse
  • Current validation: None - directly uses await response.json()
  • Zod opportunity:
    const ScoreResponseSchema = z.object({
      value: z.number(),
      region: z.string(),
      model: z.string(),
      pipeline: z.string(),
      orchestrator: z.string(),
    })
    
    const MetricSchema = z.object({
      success_rate: z.number(),
      round_trip_score: z.number(),
      score: z.number(),
    })
    
    const MetricsResponseSchema = z.record(
      z.string(),
      z.record(z.string(), MetricSchema).optional()
    )
    
    const PriceResponseSchema = z.array(z.object({
      Address: z.string(),
      ServiceURI: z.string(),
      LastRewardRound: z.number(),
      RewardCut: z.number(),
      FeeShare: z.number(),
      DelegatedStake: z.string(),
      ActivationRound: z.number(),
      DeactivationRound: z.string(),
      Active: z.boolean(),
      Status: z.string(),
      PricePerPixel: z.number(),
      UpdatedAt: z.number(),
    }))

/api/score/index.tsx

  • External APIs: Same as above
  • Current validation: None
  • Zod opportunity: Same schemas as above

/api/pipelines/index.tsx

  • External API: NEXT_PUBLIC_AI_METRICS_SERVER_URL/api/pipelines
  • Current validation: None - directly uses await response.json()
  • Zod opportunity: Use AvailablePipelinesSchema (defined in section 2.6)

/api/regions/index.ts

  • External APIs:
    • NEXT_PUBLIC_METRICS_SERVER_URL/api/regions
    • NEXT_PUBLIC_AI_METRICS_SERVER_URL/api/regions
  • Current validation: None
  • Zod opportunity: Use RegionsSchema (defined in section 2.6)

3.2 Livepeer.com API

/api/usage.tsx

  • External API: https://livepeer.com/data/usage/query/total
  • Current validation: ✅ Already uses DayDataSchema with safeParse()
  • Status: Good example of proper validation

3.3 Pinata IPFS API

/api/upload-ipfs.tsx

  • External API: https://api.pinata.cloud/pinning/pinJSONToIPFS
  • Current validation: None - directly uses await fetchResult.json()
  • Zod opportunity:
    const PinataResponseSchema = z.object({
      IpfsHash: z.string(),
      // Add other fields if Pinata returns more
    })

3.4 ENS Provider Responses

lib/api/ens.tsgetEnsForAddress()

  • External API: Ethereum provider ENS lookups
  • Current validation: None - directly uses provider responses
  • Zod opportunity: Validate ENS resolver text records and avatar URLs

4. SSR/Client-Side API Calls

4.1 Server-Side Rendering

lib/api/ssr.tsgetEnsIdentity()

  • Internal API call: /api/ens-data/${address}
  • Current validation: None - directly uses await response.json()
  • Zod opportunity: Use EnsIdentitySchema to validate the response

5. Recommended Implementation Strategy

Phase 1: Shared Schemas

  1. Create lib/api/schemas/ directory
  2. Create shared schemas for common types:
    • address.ts - Address validation
    • common.ts - Common types (strings, numbers, etc.)
    • ens.ts - ENS-related schemas
    • treasury.ts - Treasury/proposal schemas
    • performance.ts - Performance metrics schemas

Phase 2: Input Validation

  1. Add input validation to all endpoints with query params
  2. Add request body validation to POST endpoints
  3. Return proper 400 errors with validation details

Phase 3: Output Validation

  1. Add return value validation to all endpoints
  2. Log validation errors (don't fail in production, but log for monitoring)
  3. Consider failing in development mode to catch issues early

Phase 4: External API Validation

  1. Validate all external API responses
  2. Use safeParse() to handle validation errors gracefully
  3. Add retry logic or fallback values where appropriate

Example Implementation Pattern

// lib/api/schemas/address.ts
import { z } from "zod";

export const AddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/);

// lib/api/schemas/account-balance.ts
import { z } from "zod";

export const AccountBalanceSchema = z.object({
  balance: z.string(),
  allowance: z.string(),
});

// pages/api/account-balance/[address].tsx
import { AddressSchema } from "@lib/api/schemas/address";
import { AccountBalanceSchema } from "@lib/api/schemas/account-balance";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  // Validate input
  const addressResult = AddressSchema.safeParse(req.query.address);
  if (!addressResult.success) {
    return res.status(400).json({ error: "Invalid address", details: addressResult.error });
  }

  // ... fetch data ...

  // Validate output
  const result = AccountBalanceSchema.safeParse(accountBalance);
  if (!result.success) {
    console.error("Account balance validation failed:", result.error);
    // In production, might still return the data, but log the error
    // In development, could return 500 to catch issues early
  }

  return res.status(200).json(accountBalance);
};

Summary

Total Opportunities:

  • Input Validation: ~15 endpoints need query param/body validation
  • Output Validation: ~20 endpoints need return value validation
  • External API Validation: ~8 external API calls need response validation

Priority:

  1. High: External API responses (can cause runtime errors)
  2. Medium: POST request bodies (security/data integrity)
  3. Medium: Return value validation (catch contract changes)
  4. Low: Query parameter validation (basic checks exist)

Estimated Impact:

  • Better error messages for invalid inputs
  • Early detection of API contract changes
  • Protection against malformed external API responses
  • Improved type safety at runtime

Describe alternatives you've considered

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions