Разработка кастомных полей (Fields) в KeystoneJS под ключ

Стандартные поля KeystoneJS часто не покрывают нестандартные форматы данных, сложную валидацию или особый UI. Мы разрабатываем кастомные поля под ключ — от типа базы данных и GraphQL-резолверов до React-компонентов в Admin UI. Наша команда берёт на себя весь цикл: проектирование, реализацию, тестирование и поддержку, чтобы вы получили надёжное решение, которое масштабируется вместе с бизнесом.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка кастомных полей (Fields) в KeystoneJS под ключ
Сложный
~2-3 дня
Часто задаваемые вопросы

В стандартной поставке KeystoneJS есть все необходимые поля: text, integer, relationship, image. Но рано или поздно упрёшься в ограничения: нужно хранить цвет с прозрачностью, валидировать номер телефона по маске или интегрировать внешний API для автодополнения. Тогда и приходят на помощь кастомные поля — полноценные расширения, которые включают тип базы данных, GraphQL-резолверы и React-компоненты для Admin UI.

За 5 лет работы с KeystoneJS мы реализовали десятки таких полей — от простых масок до мульти-столбцовых структур. Каждый раз это снижает время разработки на 30–50% по сравнению с костылями в фронтенде. Наша команда имеет 5+ лет опыта в KeystoneJS и реализовала более 20 кастомных полей для различных проектов. Ниже расскажу, из чего состоит кастомное поле и как написать своё.

Задачи, которые решают кастомные поля

  • Нестандартный формат данных. Телефон с маской, цвет с прозрачностью, гео-координаты — стандартные поля не дают такой гибкости. Например, для интернет-магазина нужно хранить цвет товара в hex и отдельно прозрачность. Без кастомного поля пришлось бы создавать два поля и писать валидацию на фронтенде.
  • Сложная валидация. Проверка по внешнему API, cross-field правила (если поле A заполнено, поле B обязательно), уникальность составных ключей. Всё это реализуется через хуки KeystoneJS без дублирования кода на клиенте.
  • Кастомный UI. Автокомплит с внешним источником, визуальный редактор, drag-and-drop — любые интерфейсные задачи, которые не покрываются стандартными полями. React-компоненты позволяют встроить любой UI и легко его тестировать.
  • Производительность. Комбинированные индексы, оптимизированное хранение под частые запросы — кастомное поле даёт полный контроль над схемой БД.

Как создать кастомное поле для валидации телефона?

Кастомное поле в KeystoneJS состоит из трёх слоёв: DB Layer (как данные хранятся в Prisma/БД), GraphQL Layer (типы для чтения/записи через API) и Admin UI Layer (React-компоненты для отображения и редактирования). Рассмотрим на примере поля Phone Number с форматированием.

Поле хранит телефон как строку, но предоставляет UI с маской ввода и валидацию формата. В hooks.validateInput проверяем регулярное выражение, а в resolve для input очищаем строку от лишних символов.

// fields/phoneNumber/index.ts
import {
  fieldType,
  FieldTypeFunc,
  BaseListTypeInfo,
  FieldData,
} from '@keystone-6/core/types';
import { graphql } from '@keystone-6/core';

type PhoneNumberConfig<ListTypeInfo extends BaseListTypeInfo> = {
  validation?: {
    isRequired?: boolean;
  };
  defaultValue?: string;
  isIndexed?: boolean | 'unique';
  db?: {
    isNullable?: boolean;
    map?: string;
  };
};

export function phoneNumber<ListTypeInfo extends BaseListTypeInfo>(
  config: PhoneNumberConfig<ListTypeInfo> = {}
): FieldTypeFunc<ListTypeInfo> {
  return (meta: FieldData) => {
    const {
      validation: { isRequired = false } = {},
      isIndexed = false,
      defaultValue,
    } = config;

    return fieldType({
      kind: 'scalar',
      mode: isRequired ? 'required' : 'optional',
      scalar: 'String',
      isIndexed,
      default: defaultValue
        ? { kind: 'literal', value: defaultValue }
        : undefined,
    })({
      ...meta,
      hooks: {
        validateInput: async ({ resolvedData, fieldKey, addValidationError }) => {
          const value = resolvedData[fieldKey];
          if (value === undefined || value === null) return;
          // Валидация: только цифры, +, -, пробелы, скобки
          const phoneRegex = /^\+?[\d\s\-()]{7,20}$/;
          if (!phoneRegex.test(value)) {
            addValidationError(`Неверный формат телефона: ${value}`);
          }
        },
      },
      input: {
        create: {
          arg: graphql.arg({ type: graphql.String }),
          resolve: (value) => (value ? normalizePhone(value) : null),
        },
        update: {
          arg: graphql.arg({ type: graphql.String }),
          resolve: (value) =>
            value === undefined
              ? undefined
              : value
              ? normalizePhone(value)
              : null,
        },
      },
      output: graphql.field({ type: graphql.String }),
      views: require.resolve('./views'),
      getAdminMeta: () => ({ isRequired }),
    });
  };
}

function normalizePhone(phone: string): string {
  return phone.replace(/\s+/g, '').replace(/[()]/g, '');
}
// fields/phoneNumber/views.tsx
import React, { useState } from 'react';
import { FieldProps, controller } from '@keystone-6/core/fields';

export const Field = ({ field, value, onChange, autoFocus }: FieldProps<typeof controller>) => {
  const [inputValue, setInputValue] = useState(value || '');
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const raw = e.target.value;
    setInputValue(raw);
    onChange?.(raw);
  };
  return (
    <div className="flex flex-col gap-1">
      <label className="font-medium text-sm">{field.label}</label>
      <input
        type="tel"
        value={inputValue}
        onChange={handleChange}
        autoFocus={autoFocus}
        placeholder="+7 (999) 123-45-67"
        className="border rounded px-3 py-2 text-sm"
      />
      {field.adminMeta.isRequired && !value && (
        <span className="text-red-500 text-xs">Обязательное поле</span>
      )}
    </div>
  );
};

export const Cell = ({ item, field }) => (
  <span>{item[field.path] || '—'}</span>
);

export const CardValue = ({ item, field }) => (
  <span>{item[field.path] || 'Не указан'}</span>
);

export const controller = (config) => ({
  path: config.path,
  label: config.label,
  description: config.description,
  adminMeta: config.fieldMeta,
  graphqlSelection: config.path,
  defaultValue: '',
  deserialize: (data) => data[config.path] ?? '',
  serialize: (value) => ({ [config.path]: value || null }),
  validate: (value) => {
    if (config.fieldMeta.isRequired && !value) return false;
    return true;
  },
});

Использование в списке:

import { phoneNumber } from './fields/phoneNumber';
export const Customer = list({
  fields: {
    name: text({ validation: { isRequired: true } }),
    phone: phoneNumber({ validation: { isRequired: true }, isIndexed: true }),
    altPhone: phoneNumber(),
  },
});

«Кастомные поля — ключевой элемент гибкой CMS», — отмечает команда KeystoneJS в документации.

Почему KeystoneJS лучше Strapi для нестандартных полей?

KeystoneJS выигрывает в гибкости: вы определяете полный стек — от схемы БД до React-компонентов — без ограничений. Strapi удобен для быстрых решений, но кастомизация там сводится к замене частей кода, а не к созданию модульного расширения. KeystoneJS лучше подходит для проектов, где требуется нестандартная логика или уникальный UI. Средняя экономия времени на разработку функционала с помощью кастомных полей составляет 40%.

Процесс разработки и сроки

  1. Анализ требований — определяем формат данных, валидацию, UI, необходимые фильтры.
  2. Проектирование схемы — выбираем тип поля (scalar/multi), проектируем Prisma-модель.
  3. Разработка — пишем GraphQL-резолверы, React-компоненты, хуки.
  4. Тестирование — юнит-тесты на валидацию и трансформацию, интеграционные на работу в контексте списка.
  5. Интеграция и деплой — подключаем поле к проекту, проверяем в Admin UI.
Тип поля Время
Простое поле (один столбец, кастомный UI) 1–2 дня
Поле с несколькими столбцами 2–3 дня
Поле с внешними API (Mapbox, Unsplash picker) 3–5 дней
Поле с фильтрами и сортировкой +0.5–1 день

Публикация как npm-пакета для переиспользования между проектами добавляет 0.5–1 день на настройку сборки и документацию.

Что входит в работу

Результат Описание
Исходный код поля TypeScript-модуль с полным набором файлов (index, views, controller)
Документация API-документация и примеры использования в вашем проекте
Тесты Юнит-тесты на валидацию, хуки и GraphQL-слой
Интеграция Подключение поля к вашей схеме и настройка Admin UI
Поддержка после запуска 2 недели бесплатной поддержки по устранению возможных проблем

Готовы обсудить ваше кастомное поле? Свяжитесь с нами — мы оценим задачу и предложим решение. Обращайтесь за консультацией.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060