Skip to content

byte5digital/payload-assist

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Assist for Payload
Assist for Payload Tests passing License MIT

Assist for Payload

Utilities to add guardrails, DTO tooling, and ergonomic rules to Payload CMS projects.

  • Rules: Validate your Payload config at boot (e.g., kebab-case slugs).
  • DTOs: First-class helpers to define, transform, validate, and enforce DTO-only responses.
  • Ergonomics: Thin wrappers to keep endpoints and collection reads consistent and secure.

Installation

yarn add @byte5digital/payload-assist
# or
npm install @byte5digital/payload-assist

Peer deps: Payload v3+, Next v15+. Dependencies class-transformer and reflect-metadata are included in the package.

API overview

  • payloadAssist(payloadConfig, options?): Main function that initializes Assist for Payload, validates your payload config against defined rules, and returns the built config.
  • defaultConfig: Default config with built-in rules and transformAndValidate function (can be spread/overridden).
  • Dto: Base abstract class for response DTOs.
  • transformAndValidate(Dto, data): Validate Payload data and transform to DTO. Uses the configured transformer/validator.
  • withResponse(handler): Enforce DTO-only JSON responses in your custom payload endpoints.
  • withDtoReadHook([{ dto, condition }, { dto }]): Attach to collections to return DTOs from read operations with conditional DTO selection.

Helper Types

AccessControl

A comprehensive type for Payload collection access control that includes all available access control methods.

import { AccessControl } from "@byte5digital/payload-assist";

export const MyCollection: CollectionConfig = {
  slug: "my-collection",
  access: {
    admin: ({ req }) => req.user?.role === "admin",
    create: ({ req }) => req.user?.role === "admin",
    read: ({ req }) => true, // public read
    update: ({ req }) => req.user?.role === "admin",
    delete: ({ req }) => req.user?.role === "admin",
  } satisfies AccessControl,
  // ...fields
};

Usage

Initialize Assist for Payload

The main payloadAssist function initializes the library, validates your payload config against defined rules, and returns the built config. You can customize the ruleSet and transformAndValidate function through options. payloadAssist is implemenented as a wrapper function and not as a payload plugin, to ensure it validates the raw config that is set by the user, instead of the config that was previously processed by other plugins and enriched by payload.

  • ruleSet: An object map of named rules; merge defaults with your own, if required. Deactivate a default rule by setting the rule to false.
  • rules: (config: payloadConfig) => boolean | void; throw to fail with an actionable message and return true if the rule is satisfied.
  • transformAndValidate: (dto: Dto, data: unknown) => Dto Turn raw Payload data into typed DTOs.

Built-in rules:

  • disableQraphQL: Ensures GraphQL is disabled in your config
  • collectionsEndpointsUseWithResponse: Ensures all collection endpoints use withResponse
  • collectionsUseWithDtoReadHook: Ensures all collections use withDtoReadHook in their afterRead hooks
import { buildConfig } from "payload";
import payloadAssist, {
  defaultConfig,
  PayloadAssistError,
} from "@byte5digital/payload-assist";

export default payloadAssist(
  {
    // your Payload config
  },
  {
    ruleSet: {
      ...defaultConfig.ruleSet,

      // add/override rules here
      secretIsSet: (config) => {
        if (config.secret?.length > 0) return true;
        throw new PayloadAssistError("A secret needs to be set");
      },
    },
  }
);

DTOs: Purpose and usage

Define exactly what leaves your API by modeling responses as DTOs. Only explicitly exposed fields and nested DTOs are serialized. It is important that all DTOs extend the Dto class. The example below shows the usage with the default transformAndValidate.

import { Dto, Expose, Type } from "@byte5digital/payload-assist";

export class MediaResponse extends Dto {
  @Expose() url: string;
  @Expose() mimeType: string;
}

export class UserResponse extends Dto {
  @Expose() id: number;
  @Expose() name: string;
}

export class MyCollectionDto extends Dto {
  @Expose() name: string;
  @Expose() @Type(() => MediaResponse) image: MediaResponse;
  @Expose() @Type(() => UserResponse) owner: UserResponse;
}

Transform any raw Payload doc into a DTO. By default transformAndValidate uses class-transformer, but it can be configured through the payloadAssist options.

import { transformAndValidate } from "@byte5digital/payload-assist";

const payloadDoc = await getPayloadDoc();
const dto = transformAndValidate(MyCollectionDto, payloadDoc);

Enforcing DTO-only responses in endpoints

Use withResponse to guarantee your endpoints return DTOs (and nothing else). It centralizes transform/serialize and standardizes error responses. The transformation strategy is configurable.

import payload from "payload";
import {
  withResponse,
  transformAndValidate,
} from "@byte5digital/payload-assist";
import { MyDataDto } from "path/to/dtos";

export const MyCollection: CollectionConfig = {
  slug: "my-collection",
  endpoints: [
    {
      path: "/my-custom-endpoint",
      method: "get",
      handler: withResponse(async (req: PayloadRequest) => {
        const myData = await req.payload.find({
          collection: "my-collection",
          // ...additional filter logic
        });

        const myDataDto = transformAndValidate(MyDataDto, myData);
        return {
          response: myDataDto,
          status: 200,
        };
      }),
    },
  ],

  // ...fields
};

Enforcing DTO-only responses from collections

Attach withDtoReadHook to your collections afterReadHooks to transform read results automatically to the given DTOs. Multiple objects with DTOs can be passed, when they include a condition, only the last item can be without a condition. The first DTO where the condition is met will be used or the DTO without a condition, if given. If none applies, null will be returned. So the order of the given DTOs should be: More specific first, default last.

// src/collections/MyCollection.ts
import { CollectionConfig } from "payload/types";
import { withDtoReadHook } from "@byte5digital/payload-assist";
import { MyCollectionDto, MyCollectionAdminDto } from "path/to/dtos";

export const MyCollection: CollectionConfig = {
  slug: "my-collection",
  hooks: {
    afterRead: [
      withDtoReadHook(
        {
          dto: MyCollectionAdminDto,
          condition: ({ req: { user } }) => user?.role === "admin",
        },
        {
          dto: MyCollectionDto,
        }
      ),
    ],
  },
  // ...fields
};

License

MIT

About byte5

We're a development company based in Frankfurt, Germany — remote-friendly, open-minded, and tech-driven. Our team brings deep expertise in Node.js, MedusaJS, Laravel, Umbraco, and decentralized tech like IOTA. We collaborate with clients who care about clean code, scalable solutions, and long-term maintainability.

We contribute to open source, run Laravel DACH Meetups, and support developer communities across the DACH region. Our expertise in e-commerce platforms makes us the perfect partner for building robust, scalable solutions.

If you love building smart solutions with real impact — we should talk.

Connect with us:

Support


Built with 🩵 by byte5

About

Assist for Payload adds guardrails, DTO tooling, and ergonomic rules to Payload CMS projects.

Resources

License

Stars

Watchers

Forks

Contributors 3

  •  
  •  
  •