Last modified: August 22, 2025
@hubspot/cms-dev-server is a package that facilitates starting an Express + Vite development server, enabling an auto-reloading local development workflow that is nearly identical to your deployed components. The cms-dev-server also enables rendering local versions of your components on live CMS pages to aid in development.

Basic usage

You can start a local development server by running hs-cms-dev-server/path/to/components-directory in a project that has @hubspot/cms-dev-server installed. For example, a CMS page https://cmssite.com/page with JS rendered components would be accessible by visiting one of:
  • http://cmssite.com.hslocal.net:3000/page
  • http://cmssite.com.localhost:3000/page
Or by visiting http://hslocal.net:3000/proxy and pasting in the page you want to proxy. Similar to how previewing a page from the page editor works, you can force the page to render with the context of a known contact by passing an email parameter. For example http://cmssite.com.hslocal.net:3000/page?email=bh@hubspot.com will cause the contact object to be populated based on the email parameter value. You may also start the dev server with the --ssl option, which enables:
  • https://cmssite.com.hslocal.net:3000/page
  • https://cmssite.com.localhost:3000/page

Routes

When the development server starts, it will look at the components/modules directory, then will dynamically create routes based on the modules that it finds. There are two routes for previewing your modules: /module/moduleName and /preview/module/moduleName.

/module/moduleName

http/module/moduleName The /module/moduleName route is rendered entirely locally without connecting with the HubSpot backend. This enables you to work offline and unauthenticated at this route. Field values that are used are derived entirely from the Field default values and from parameter-level overrides. Parameter-level overrides can be passed via the fields parameter, which expects stringified JSON of fieldValues that matches the passed fieldValues prop (matching the fields definition structure). Using this route includes some caveats for querying data:
  • GraphQL data in this context is fetched from your local machine using your local access token to authorize the collector service requests. These queries are cached, but you can bust the cache with the ?hsLocalQueryKey query parameter.
  • hublDataTemplate is not supported at this route.
  • Icon, CTA, and other @hubspot/cms-component field helpers are not supported at this route.

/preview/module/moduleName

The /preview/module/moduleName route interacts with the HubSpot backend and behaves similarly to viewing a module in the design manager previewer. Field values that are used rely on defaults, as there is no module instance to pull from. There is no fields parameter available for overrides. When querying data on this route:
  • GraphQL data is derived on the backend, and there is no query from the local server to the GraphQL service.
  • hublDataTemplate is supported.
  • Icon, CTA, and other @hubspot/cms-component field helpers are supported.

Proxying private pages

With the local development server running, you can preview your local changes on live HubSpot pages, including private pages that require passwords or login to access, as well as page previews.

Proxied membership pages

To view your local changes on a proxied membership page, first visit the page and log in with the configured method. Once authenticated, add hslocal.net:3000 or localhost:3000 to the root domain, just as you would with proxying a public page. To view the page as a specific contact, you can add an email query parameter to the URL, followed by the contact’s email address, as shown below: https://mydomain.hslocal.net:3000/private-page-path?email=hi@hubspot.com That email will be used for the request_contact HubL variable, and can be passed to React using hublDataTemplate.

Proxied preview

To view your local changes on a proxied version of a page preview, first navigate to the page editor in HubSpot, then click Preview in the upper right, then Open in a new tab.
page-editor-open-preview
With the page preview open in a new tab, add hslocal.net:3000 or localhost:3000 to the root domain. To view the page as a specific contact, you can add an email query parameter to the URL, followed by the contact’s email address, as shown below: https://mydomain.hslocal.net:3000/private-page-path?hs_preview=[preview_key]&email=hi@hubspot.com

Storybook

The local development server includes a Storybook integration which allows you to start a Storybook instance alongside the development server. Include a --storybook option when running the command to start the local development server. Once started, you can add .stories.jsx files alongside your components to build stories for testing or development. At the root http://hslocal.net:3000 page, a link will be included to the Storybook UI for your project. To make building stories for HubSpot modules easier, cms-dev-server provides helpers to auto-generate argTypes based on module fields. See the GraphQL and Storybook example project for usage of moduleStory(). Storybook is built with client components in mind, so components that cross island boundaries can have unexpected lifecycle behavior when rendered in a story. Because server-only components never make it to the browser, they cannot be hot reloaded, so a full re-render is necessary to update the server response. To fully emulate hybrid rendering in Storybook at the cost of hot module reloading, you can use moduleStoryWithIsland() in your story in place of moduleStory().

Fields Type Generation

If you’re using TypeScript in your project, you can use the --generateFieldTypes argument when starting the development server. This command will watch for changes to the fields object that is exported from module files and create a .types.ts file inside of the module directory. You can then import this module directly in your module component and use it in the generate ModuleProps<T> type. For example, suppose you have the following fields.jsx file:
export const fields = (
  <ModuleFields>
    <ChoiceField
      label="Choice Test"
      name="choice"
      display="select"
      choices={[
        ['choice1', 'One'],
        ['choice2', 'Two'],
      ]}
      default="choice1"
    />
    <NumberField label="Display on each blog post" name="numberField" />
    <FieldGroup name="defaultGroup" label="Default text" locked>
      <TextField
        label="Text Field One"
        name="textFieldOne"
        default="Text Field"
      />
      <TextField label="Text Field Two" name="textFieldTwo" />
      <NumberField label="Number Field" name="numberField" />
    </FieldGroup>
  </ModuleFields>
);
Running hs-cms-dev-server /path/to/project --generateFieldsTypes will generate a modules/MyModule/fields.types.ts file with a default exported type MyModuleFieldsType. The above fields.tsx will generate the following file:
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
// Removing the above comment will disable type generation for this module
// This file was created by @hubspot/cms-dev-server, for more information see https://github.hubspot.com/cms-react/reference/js-modules.html#module-fields
import { type DefaultValues, type ChoiceFieldType, type NumberFieldType, type TextFieldType, type GroupFieldType, type Override } from "@hubspot/cms-components/fields";
type MyModuleFieldsType = DefaultValues<{
    choice: Required<ChoiceFieldType>;
    numberField: NumberFieldType;
    defaultGroup: Override<GroupFieldType, {
        children: {
            textFieldOne: Required<TextFieldType>;
            textFieldTwo: TextFieldType;
            numberField: NumberFieldType;
        };
    }>;
}>;
export default MyModuleFieldsType;
Then, you can import and use the type in your component as follows:
import { ModuleProps } from '@hubspot/cms-components';
import MyModule from './fields.types';

export const Component = ({
  fieldValues,
  hublParameters = {},
}: ModuleProps<MyModule>) => {
  const number = fieldValues.numberField;
  //   ^?const number: number | number[] | null | undefined
  //   Note that can be undefined because no default set

  const choice = fieldValues.choice;
  //   ^?const choice: string | number | (string | number)[]

  const text = fieldValues.defaultGroup.textFieldOne
  //   ^?const text: string | null
}
Note that the generated fields.types.ts file will be overwritten every time there is an update made to the fields object. To disable this behavior, remove the THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY comment at the top of the file.

Secrets in local development

To make secrets available for local development via @hubspot/cms-dev-server, create a .env file to define secret values for local use only. Keys in this file need to be prefixed with HS_ for the development server to recognize it as a secret.
HS_TEST_SECRET=localsecretvalue
This secret will be accessible locally without the HS_ prefix you included in your .env file. In the example above, the secret can be accessed with getSecret('TEST_SECRET').