Feature request form on website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementing a Feature Request Form on a Website

A Feature Request form is not just a "text field." A well-implemented form structures user requests: separates problem description from proposed solutions, gathers context (who's asking, how often they encounter the problem), and lets the team prioritize the backlog without phone calls.

Form Structure

A minimal set of fields that provides useful signal:

  • Request title (short, the essence)
  • Problem description (what task are you trying to solve, not "add a button" but why)
  • Proposed solution (optional)
  • Category / product area
  • Importance assessment (how often you encounter the problem)
// FeatureRequestForm.tsx (React + React Hook Form + Zod)
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  title: z
    .string()
    .min(10, 'Minimum 10 characters')
    .max(120, 'Maximum 120 characters'),
  problem: z
    .string()
    .min(30, 'Describe the problem in more detail')
    .max(2000),
  solution: z.string().max(2000).optional(),
  category: z.enum(['ui-ux', 'performance', 'integrations', 'api', 'other']),
  importance: z.enum(['critical', 'high', 'medium', 'low']),
  email: z.string().email().optional().or(z.literal('')),
});

type FormData = z.infer<typeof schema>;

const CATEGORIES = [
  { value: 'ui-ux', label: 'Interface / UX' },
  { value: 'performance', label: 'Performance' },
  { value: 'integrations', label: 'Integrations' },
  { value: 'api', label: 'API / Developers' },
  { value: 'other', label: 'Other' },
] as const;

const IMPORTANCE = [
  { value: 'critical', label: 'Critical — can\'t work without this' },
  { value: 'high', label: 'High — encounter it every day' },
  { value: 'medium', label: 'Medium — inconvenient but tolerable' },
  { value: 'low', label: 'Low — would be nice to have' },
] as const;

export function FeatureRequestForm() {
  const {
    register,
    control,
    handleSubmit,
    formState: { errors, isSubmitting, isSubmitSuccessful },
    reset,
    watch,
  } = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: {
      category: 'other',
      importance: 'medium',
    },
  });

  const titleValue = watch('title', '');

  const onSubmit = async (data: FormData) => {
    const res = await fetch('/api/feature-requests', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...data,
        submittedAt: new Date().toISOString(),
        pageUrl: window.location.href,
      }),
    });

    if (!res.ok) {
      const err = await res.json();
      throw new Error(err.message ?? 'Error submitting request');
    }
  };

  if (isSubmitSuccessful) {
    return (
      <div className="rounded-lg border border-green-200 bg-green-50 p-6 text-center">
        <p className="text-lg font-semibold text-green-800">Request submitted</p>
        <p className="mt-2 text-sm text-green-700">
          We'll consider it when planning the next release.
        </p>
        <button
          onClick={() => reset()}
          className="mt-4 text-sm text-green-700 underline"
        >
          Submit another
        </button>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-5 max-w-xl">
      {/* Title */}
      <div>
        <label className="block text-sm font-medium mb-1">
          Briefly describe the request
          <span className="text-gray-400 ml-1 font-normal">
            ({titleValue.length}/120)
          </span>
        </label>
        <input
          {...register('title')}
          type="text"
          placeholder="Example: Export data to CSV"
          className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        {errors.title && (
          <p className="mt-1 text-xs text-red-600">{errors.title.message}</p>
        )}
      </div>

      {/* Problem Description */}
      <div>
        <label className="block text-sm font-medium mb-1">
          What problem does this solve?
        </label>
        <p className="text-xs text-gray-500 mb-1">
          Describe the situation, not the specific solution — this helps us find a better approach
        </p>
        <textarea
          {...register('problem')}
          rows={4}
          placeholder="When I try to do X, I have to Y, which is inconvenient because..."
          className="w-full border rounded-md px-3 py-2 text-sm resize-y focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        {errors.problem && (
          <p className="mt-1 text-xs text-red-600">{errors.problem.message}</p>
        )}
      </div>

      {/* Proposed Solution */}
      <div>
        <label className="block text-sm font-medium mb-1">
          How would you implement this? <span className="text-gray-400">(optional)</span>
        </label>
        <textarea
          {...register('solution')}
          rows={3}
          placeholder="Add an 'Export' button to the table menu that downloads..."
          className="w-full border rounded-md px-3 py-2 text-sm resize-y focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
      </div>

      {/* Category */}
      <div>
        <label className="block text-sm font-medium mb-2">Product area</label>
        <Controller
          control={control}
          name="category"
          render={({ field }) => (
            <div className="flex flex-wrap gap-2">
              {CATEGORIES.map(cat => (
                <button
                  key={cat.value}
                  type="button"
                  onClick={() => field.onChange(cat.value)}
                  className={`px-3 py-1.5 rounded-full text-xs border transition-colors ${
                    field.value === cat.value
                      ? 'bg-blue-600 border-blue-600 text-white'
                      : 'border-gray-300 hover:border-blue-400'
                  }`}
                >
                  {cat.label}
                </button>
              ))}
            </div>
          )}
        />
      </div>

      {/* Importance */}
      <div>
        <label className="block text-sm font-medium mb-2">How important is this for you?</label>
        <div className="space-y-2">
          {IMPORTANCE.map(item => (
            <label key={item.value} className="flex items-start gap-2 cursor-pointer">
              <input
                {...register('importance')}
                type="radio"
                value={item.value}
                className="mt-0.5"
              />
              <span className="text-sm">{item.label}</span>
            </label>
          ))}
        </div>
      </div>

      {/* Email */}
      <div>
        <label className="block text-sm font-medium mb-1">
          Email <span className="text-gray-400">(to notify you when implemented)</span>
        </label>
        <input
          {...register('email')}
          type="email"
          placeholder="[email protected]"
          className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        {errors.email && (
          <p className="mt-1 text-xs text-red-600">{errors.email.message}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium py-2.5 rounded-md text-sm transition-colors"
      >
        {isSubmitting ? 'Submitting...' : 'Submit request'}
      </button>
    </form>
  );
}

API Endpoint

// pages/api/feature-requests.ts (Next.js) or routes/feature-requests.ts (Express)
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') return res.status(405).end();

  const { title, problem, solution, category, importance, email, pageUrl } = req.body;

  // Basic validation
  if (!title || !problem || !category || !importance) {
    return res.status(400).json({ message: 'Required fields not filled' });
  }

  const record = await db.featureRequest.create({
    data: {
      title,
      problem,
      solution: solution || null,
      category,
      importance,
      email: email || null,
      pageUrl,
      status: 'new',
      votes: 0,
    },
  });

  // Notification to Linear/Jira/Notion via webhook
  if (process.env.LINEAR_WEBHOOK_URL) {
    await fetch(process.env.LINEAR_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        title: `[Feature] ${title}`,
        description: `**Problem:**\n${problem}\n\n**Solution:**\n${solution ?? 'not specified'}`,
        priority: importance === 'critical' ? 1 : importance === 'high' ? 2 : 3,
        labelIds: [CATEGORY_LABEL_MAP[category]],
      }),
    });
  }

  return res.status(201).json({ id: record.id });
}

Voting System Integration

If you plan to add upvotes to requests, design the form immediately with a unique record ID and a /roadmap or /feature-requests page where users can see and vote on existing requests. This prevents duplicates and gathers real priority signals.

Timeline

Form with validation, API, and notifications — two to three days. Adding deduplication (search for similar requests before submission via simple text search), a page with request list, and public voting — another three to five days.