Skip to content

Add Zod validation middleware to all backend routes — replace manual checks #193

@Calebux

Description

@Calebux

Description

Some backend routes validate input with Zod schemas, but many routes perform manual checks or no validation at all. Inconsistent validation means some endpoints accept malformed data that causes database errors or unexpected behavior. All routes should use a consistent Zod middleware pattern.

Current Inconsistency

// Some routes — proper Zod validation
const schema = z.object({ name: z.string().min(1), price: z.number().positive() });
const result = schema.safeParse(req.body);
if (\!result.success) return res.status(422).json({ errors: result.error.issues });

// Other routes — no validation
router.post('/', authenticate, async (req, res) => {
  const { name, price, billingCycle } = req.body; // ← no validation
  await supabase.from('subscriptions').insert({ name, price, billing_cycle: billingCycle });
});

Solution: Shared validation middleware factory

/backend/src/middleware/validate.ts

import { ZodSchema } from 'zod';
import { Request, Response, NextFunction } from 'express';

export function validate(schema: ZodSchema, source: 'body' | 'query' | 'params' = 'body') {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req[source]);
    if (\!result.success) {
      return res.status(422).json({
        type: 'https://syncro.app/errors/validation',
        title: 'Validation Error',
        status: 422,
        errors: result.error.issues.map(i => ({
          field: i.path.join('.'),
          message: i.message,
        })),
      });
    }
    req[source] = result.data; // replace with parsed/coerced data
    next();
  };
}

Usage

const createSubscriptionSchema = z.object({
  name: z.string().min(1).max(100),
  price: z.number().positive().max(100000),
  currency: z.string().length(3),
  billing_cycle: z.enum(['monthly', 'quarterly', 'annual', 'weekly']),
  next_renewal_date: z.string().datetime(),
  renewal_url: z.string().url().max(2000).optional(),
  category: z.string().max(50).optional(),
});

router.post('/', authenticate, validate(createSubscriptionSchema), createSubscription);

All Routes Needing Schemas

  • POST /api/subscriptions — createSubscriptionSchema
  • PUT /api/subscriptions/:id — updateSubscriptionSchema
  • POST /api/team/invite — inviteTeamSchema
  • POST /api/keys — createApiKeySchema
  • POST /api/webhooks — createWebhookSchema
  • All other POST/PUT endpoints

Acceptance Criteria

  • validate() middleware created and exported
  • All POST and PUT routes use the middleware
  • Validation errors return consistent Problem Details format
  • All schemas have explicit max lengths on string fields
  • Query parameter validation added to GET routes with filters

Metadata

Metadata

Assignees

No one assigned

    Labels

    BackendStellar WaveIssues in the Stellar wave programsecuritySecurity vulnerability or concern

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions