Add Field to Existing Next.js Feature
Not for new features or pages — use @frontend-next/create-feature or @frontend-next/create-page.
Reference: src/features/{name}/ or related components/, services/, types/.
Checklist
[ ] 1. Types — entity interface + form/payload types
[ ] 2. Zod schema — validation in utils/ or feature folder
[ ] 3. Form — FormField or controlled input + defaultValues
[ ] 4. API service — include field in request body/params
[ ] 5. Display — product card, detail page, checkout summary, etc.
[ ] 6. Store (if persisted) — Zustand slice field + actions
[ ] 7. i18n — labels in messages/en.json + ar.json if user-facing
[ ] 8. Server fetch — update RSC fetch types/select if SSR page
Step 1 — Types
// features/product/types/product.types.ts
export interface Product {
id: string;
name_en: string;
name_ar: string;
sku?: string | null;
isFeatured: boolean;
}
export interface UpdateProfilePayload {
phone?: string;
companyName?: string; // new
}
Keep types aligned with backend DTOs and API responses.
Step 2 — Zod schema
See @frontend-next/forms.
// utils/validation.ts or features/checkout/utils/validation.ts
export const profileSchema = z.object({
phone: z.string().min(1),
companyName: z.string().optional(),
});
| Backend type | Zod |
|---|---|
| optional string | z.string().optional() |
z.string().email() |
|
| phone | custom refine with libphonenumber-js |
| conditional required | .refine() on object |
Step 3 — Form input
<FormField control={form.control} name="companyName" render={({ field }) => (
<FormItem>
<FormLabel>{t('companyName')}</FormLabel>
<FormControl><Input {...field} value={field.value ?? ''} /></FormControl>
<FormMessage />
</FormItem>
)} />
Auth forms may use controlled useState — add the field there instead of RHF.
File inputs — separate useState<File | null>, not in RHF.
Prefill from auth/API:
useEffect(() => {
if (user) form.reset({ ...form.getValues(), companyName: user.companyName ?? '' });
}, [user]);
Step 4 — API layer
See @frontend-next/api-layer.
Client service (mutations):
export const updateProfile = (payload: UpdateProfilePayload) =>
api.patch('/user/update-profile', payload);
Server fetch (RSC pages) — extend return type and ensure backend includes the field:
// lib/api/products.ts
export async function getProduct(slug: string): Promise<Product> { ... }
Step 5 — Display components
Update every UI surface that shows the entity:
| Surface | Action |
|---|---|
| Detail page | render new field |
| Card / list item | show if relevant (badge, subtitle) |
| Checkout summary | include in line items or address block |
| Order confirmation | display persisted value |
Use locale-aware field for bilingual data:
const name = locale.startsWith('ar') ? product.name_ar : product.name_en;
Step 6 — Zustand store (if applicable)
See @frontend-next/state-management.
interface CartItem {
productId: string;
quantity: number;
giftMessage?: string; // new
}
Update actions that create/update the item shape.
Step 7 — i18n
See @frontend-next/i18n.
// messages/en.json
{ "companyName": "Company name" }
// messages/ar.json
{ "companyName": "اسم الشركة" }
Only add translations for user-visible labels — not internal admin fields on storefront unless shown.
Step 8 — SEO / metadata (if public field)
If the new field affects page content (e.g. metaDescription), update generateMetadata — see @frontend-next/seo.
Field-type quick reference
| Field | Types | Form | Display |
|---|---|---|---|
| String | string? |
Input, Textarea |
text |
| Boolean | boolean |
Checkbox |
conditional UI |
| Enum | union | Select, RadioGroup |
mapped label |
| Price/money | number |
formatted input | formatPrice() |
| Date | string ISO |
date picker | formatted date |
| Image URL | string |
file state | next/image + getMediaUrl |
| Bilingual | name_en/name_ar |
both inputs (admin) or locale pick (display) | locale switch |
Verification
[ ] Types match API response
[ ] Form validates and submits new field
[ ] Client mutation payload correct
[ ] Server-rendered pages show new field (if applicable)
[ ] Translations added for user-facing labels
[ ] No stale React Query cache — invalidate relevant keys