Skip to content
cccskills
FreePublished on GitHub

frontend-form-pattern

Frontend form patterns — form types in model/, useForm setup, and end-to-end backend validation with field-level error display. Use when adding or modifying any form (page or dialog) that submits to the API.

The English summary is being prepared. Showing the original description.

ayunis-core/ayunis-core34 stars(GitHub 34 + cccskills 0)

Run score Not tested

See how to install

Files included (1)

  • SKILL.md8.4 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Frontend Form Pattern

This skill covers how forms are structured in page modules and the complete validation flow from backend DTO → API response → frontend form field errors with i18n.

Form Types

Form field interfaces live in the page module's model/ directory — never inline in the component file.

// model/types.ts
export interface ThingFormFields {
  name: string;
  description: string;
}

Multiple form types can share the same file (e.g. CreateThingFormFields and UpdateThingFormFields), or use separate files if they diverge significantly.

The component imports the type:

// ui/ThingDetailPage.tsx
import type { ThingFormFields } from '../model/types';

const form = useForm<ThingFormFields>({
  defaultValues: { name: entity.name, description: entity.description ?? '' },
});

Existing examples

  • pages/admin-settings/teams-settings/model/types.ts — CreateTeamFormData, UpdateTeamFormData
  • pages/admin-settings/letterhead-detail/model/types.ts — LetterheadFormFields
  • pages/prompts/model/createPromptSchema.ts — Zod schema with inferred type

Page Component Structure

Page components (ui/) should contain only component logic: state, hooks, form setup, event handlers, and JSX. Keep them focused:

  • Form types → model/types.ts
  • Form setup (useForm, defaultValues, onSubmit) stays in the component — it's component logic
  • Pure helper functions → lib/ (see the ayunis-core-frontend-dev skill)

Validation

Overview

Backend DTO (@MaxLength, @IsNotEmpty, …)
  ↓ ValidationPipe rejects
  ↓ exceptionFactory in main.ts formats response
  ↓
{ code: "VALIDATION_ERROR", errors: [{ field, constraints }] }
  ↓
Frontend hook: extractErrorData() → setValidationErrors(form, …)
  ↓
form.setError(field, { message: t("validation.field.constraint") })
  ↓
<FormMessage /> renders inline error

Backend

DTO (class-validator decorators)

Validation rules live on the DTO. These are the source of truth — they also feed the OpenAPI spec and generated frontend client.

// presenters/http/dto/create-thing.dto.ts
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';

export class CreateThingDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(100)
  name: string;

  @IsString()
  @IsNotEmpty()
  @MaxLength(500)
  description: string;
}

Global ValidationPipe (already configured in main.ts)

The exceptionFactory normalizes validation errors into a structured response with a VALIDATION_ERROR code and field-level detail. You don't need to touch this — it applies globally to all DTOs.

Response format:

{
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "errors": [
    { "field": "description", "constraints": ["maxLength"] }
  ]
}

Nested DTOs produce dot-notation field names (e.g. "address.street").

Domain errors (ApplicationError)

Domain-level errors (e.g. duplicate name, not found) are separate from DTO validation. They use the ApplicationError pattern with module-specific error codes:

// application/thing.errors.ts
export class DuplicateThingNameError extends ThingError {
  constructor(name: string) {
    super(`Thing "${name}" already exists`, ThingErrorCode.DUPLICATE_NAME, 409);
  }
}

These flow through ApplicationErrorFilter and arrive at the frontend with their own code.

Frontend

extractErrorData (shared, already exists)

Extracts code, message, status, and errors from Axios errors:

import extractErrorData from '@/shared/api/extract-error-data';
// Returns: { code, message, status, errors?: FieldError[] }

setValidationErrors (shared helper)

Maps backend field errors to form.setError() with translated messages.

import { setValidationErrors } from '@/shared/lib/set-validation-errors';

Translation key resolution order:

  1. ${prefix}.${field}.${constraint} — e.g. validation.description.maxLength
  2. ${prefix}.${field}.invalid — e.g. validation.description.invalid
  3. ${prefix}.invalid — generic fallback

Hook pattern

The hook receives the form instance so it can set field errors on validation failure. Other error codes (domain errors) show toast messages as before.

// api/useCreateThing.ts
import type { UseFormReturn } from 'react-hook-form';
import { showSuccess, showError } from '@/shared/lib/toast';
import extractErrorData from '@/shared/api/extract-error-data';
import { setValidationErrors } from '@/shared/lib/set-validation-errors';

export function useCreateThing(
  form: UseFormReturn<ThingFormData>,
  onSuccess?: () => void,
) {
  const { t } = useTranslation('things');
  const mutation = useThingsControllerCreate({
    mutation: {
      onSuccess: async () => {
        showSuccess(t('toast.createSuccess'));
        onSuccess?.();
      },
      onError: (error: unknown) => {
        try {
          const { code, errors } = extractErrorData(error);
          if (code === 'VALIDATION_ERROR' && errors) {
            setValidationErrors(form, errors, t, 'validation');
          } else if (code === 'DUPLICATE_NAME') {
            showError(t('toast.duplicateName'));
          } else {
            showError(t('toast.createError'));
          }
        } catch {
          showError(t('toast.createError'));
        }
      },
    },
  });

  return {
    createThing: (data: CreateThingDto) => mutation.mutate({ data }),
    isCreating: mutation.isPending,
  };
}

If a hook is used without a form (e.g. a toggle), make form optional and guard the setValidationErrors call:

export function useUpdateThing(
  form?: UseFormReturn<ThingFormData>,
  onSuccess?: () => void,
) {
  // ...
  if (code === 'VALIDATION_ERROR' && errors && form) {
    setValidationErrors(form, errors, t, 'validation');
  }
}

Dialog component

Pass the form to the hook:

const form = useForm<ThingFormData>({ defaultValues: { name: '', description: '' } });
const { createThing, isCreating } = useCreateThing(form, () => {
  onOpenChange(false);
  form.reset();
});

The form fields must use <FormMessage /> (from shadcn) to render errors inline — this is already standard in all form dialogs.

Translation keys

Add a validation section to the page's translation namespace:

{
  "validation": {
    "name": {
      "isNotEmpty": "Name is required",
      "maxLength": "Name must be 100 characters or fewer",
      "invalid": "Invalid name"
    },
    "description": {
      "isNotEmpty": "Description is required",
      "maxLength": "Description must be 500 characters or fewer",
      "invalid": "Invalid description"
    },
    "invalid": "Invalid input"
  }
}

Constraint keys match the class-validator decorator names in camelCase: isNotEmpty, maxLength, isString, isEnum, isEmail, minLength, length, matches, etc.

Checklist

  • Form types defined in model/types.ts, not inline in the component
  • DTO has class-validator decorators for all constraints
  • Hook accepts form parameter and calls setValidationErrors on VALIDATION_ERROR
  • Hook still handles domain error codes (e.g. duplicate) via toast
  • Dialog passes form to the hook
  • Translation file has validation.{field}.{constraint} keys in all locales (en, de)
  • Form fields use <FormMessage /> for inline error display

Reference implementation

See the skill templates feature for a complete example:

  • Backend DTO: ayunis-core-backend/src/domain/skill-templates/presenters/http/dto/create-skill-template.dto.ts
  • Frontend hook: ayunis-core-frontend/src/pages/super-admin-settings/skill-templates/api/useCreateSkillTemplate.ts
  • Frontend dialog: ayunis-core-frontend/src/pages/super-admin-settings/skill-templates/ui/CreateSkillTemplateDialog.tsx
  • Translations: ayunis-core-frontend/src/shared/locales/en/super-admin-settings-skills.json
  • Shared helper: ayunis-core-frontend/src/shared/lib/set-validation-errors.ts
  • Error extractor: ayunis-core-frontend/src/shared/api/extract-error-data.ts
  • ValidationPipe config: ayunis-core-backend/src/main.ts (exceptionFactory)

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Query Ayunis deployed-environment telemetry in AppSignal — incidents, occurrence counts and error traces via the `appsignal-cli` binary; structured log attributes (user.id, org.id, nestjs.context, custom metadata) via the v2 logs API. Use when investigating a production/staging incident or log data — NOT for local dev logs (see backend-debugging).

The English summary is being prepared. Showing the original description.

ayunis-core/ayunis-core34Updated Oct 26, 2026

Ayunis Core backend specifics — module boundaries, key files, and project conventions. Complements the nestjs-hexagonal-backend skill.

The English summary is being prepared. Showing the original description.

ayunis-core/ayunis-core34Updated Oct 26, 2026

Frontend development in ayunis-core. Use when creating, modifying, or debugging frontend code (React, Feature-Sliced Design, API client).

The English summary is being prepared. Showing the original description.

ayunis-core/ayunis-core34Updated Oct 26, 2026

Debug backend runtime errors (500s, crashes, unexpected behavior). Use when something is broken at runtime — not for writing new code.

The English summary is being prepared. Showing the original description.

ayunis-core/ayunis-core34Updated Oct 26, 2026

Shared test fixtures for backend specs — port mock factories, domain builders, and shared IDs in a per-module testing/ folder. Use when a module's *.use-case.spec.ts files repeat the same mock-repository literal, inline entity construction, or hardcoded UUIDs across files.

The English summary is being prepared. Showing the original description.

ayunis-core/ayunis-core34Updated Oct 26, 2026

Analyze Cursor Bugbot PR comments through a root-cause lens: is the finding valid, and is the suggested fix the real fix or a bandaid over a deeper problem? Use when the user asks to check, assess, or triage bugbot comments.

The English summary is being prepared. Showing the original description.

ayunis-core/ayunis-core34Updated Oct 26, 2026

See all skills by ayunis-core

Report a problem with this skill