<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tommaso Meli</title>
    <description>The latest articles on DEV Community by Tommaso Meli (@tommasomeli).</description>
    <link>https://dev.to/tommasomeli</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3948011%2F071d651b-54df-4d17-ad07-44a5fefdb29b.png</url>
      <title>DEV Community: Tommaso Meli</title>
      <link>https://dev.to/tommasomeli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tommasomeli"/>
    <language>en</language>
    <item>
      <title>Prisma Generator NestJS DTO — pluggable DTOs with annotations and custom generators</title>
      <dc:creator>Tommaso Meli</dc:creator>
      <pubDate>Sat, 23 May 2026 18:41:08 +0000</pubDate>
      <link>https://dev.to/tommasomeli/prisma-generator-nestjs-dto-pluggable-dtos-with-annotations-and-custom-generators-26d8</link>
      <guid>https://dev.to/tommasomeli/prisma-generator-nestjs-dto-pluggable-dtos-with-annotations-and-custom-generators-26d8</guid>
      <description>&lt;p&gt;If you build NestJS APIs on top of Prisma, you've probably felt the friction: your schema is the source of truth, but your DTOs, validation rules, and Swagger metadata live somewhere else — and they drift.&lt;/p&gt;

&lt;p&gt;Most Prisma DTO generators solve the basics (Create / Update / Entity classes with decorators). That's useful, but the moment you need &lt;strong&gt;custom output&lt;/strong&gt; — audit metadata, GraphQL types, RBAC manifests, your own validators — you're back to hand-written glue code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@tommasomeli/prisma-generator-nestjs-dto" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;@tommasomeli/prisma-generator-nestjs-dto&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is a Prisma generator built for that second phase. It emits the usual NestJS DTOs, then gets out of your way — with a &lt;strong&gt;plugin API&lt;/strong&gt;, &lt;strong&gt;annotation-driven decorators&lt;/strong&gt;, and a &lt;strong&gt;type-safe config file&lt;/strong&gt; when the Prisma schema block isn't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get out of the box
&lt;/h2&gt;

&lt;p&gt;For every Prisma model &lt;code&gt;User&lt;/code&gt;, run &lt;code&gt;npx prisma generate&lt;/code&gt; and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/generated/nestjs-dto/
  user/
    user.entity.ts
    create-user.dto.ts
    update-user.dto.ts
    index.ts
  index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;class-validator&lt;/code&gt;&lt;/strong&gt; decorators (&lt;code&gt;@IsString&lt;/code&gt;, &lt;code&gt;@IsOptional&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@nestjs/swagger&lt;/code&gt;&lt;/strong&gt; metadata (&lt;code&gt;@ApiProperty&lt;/code&gt;, &lt;code&gt;@ApiHideProperty&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-contained imports&lt;/strong&gt; — relation DTOs, &lt;code&gt;@DtoOverrideType&lt;/code&gt; targets, and annotation arguments are resolved automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configure it directly in &lt;code&gt;schema.prisma&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generator nestjsDto {
  provider           = "prisma-generator-nestjs-dto"
  output             = "../src/generated/nestjs-dto"
  outputType         = "class"
  outputStructure    = "nestjs"
  fileNamingStrategy = "kebab"
  reExport           = "true"
  classValidator     = "true"
  swaggerDocs        = "true"
  prettier           = "true"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; @tommasomeli/prisma-generator-nestjs-dto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Annotations — control visibility without touching generated code
&lt;/h2&gt;

&lt;p&gt;Triple-slash comments (&lt;code&gt;///&lt;/code&gt;) above models and fields drive the built-in generators. No post-processing, no manual edits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
  id           Int    @id @default(autoincrement())
  email        String @unique
  name         String
  /// @DtoHidden
  passwordHash String
  /// @DtoReadOnly
  createdAt    DateTime @default(now())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DtoHidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hide everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DtoReadOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exclude from Create and Update DTOs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DtoEntityHidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hide in the Entity (API response)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@DtoCreateHidden&lt;/code&gt; / &lt;code&gt;@DtoUpdateHidden&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Hide in one DTO only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DtoOverrideType(MyType)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Override the TypeScript type (auto-imported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DtoIgnoreModel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skip the model entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also bind &lt;strong&gt;your own&lt;/strong&gt; validators and decorators via config — more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plugin system — when built-in DTOs aren't enough
&lt;/h2&gt;

&lt;p&gt;The differentiator is &lt;code&gt;extraGenerators&lt;/code&gt;: drop in any class extending &lt;code&gt;BaseGenerator&lt;/code&gt; and it runs alongside the built-ins in the same pipeline.&lt;/p&gt;

&lt;p&gt;A plugin receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The parsed model graph (with annotations on every field)&lt;/li&gt;
&lt;li&gt;Resolved config (&lt;code&gt;extraDecorators&lt;/code&gt;, &lt;code&gt;extraValidators&lt;/code&gt;, &lt;code&gt;extraImports&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;Import-merging helpers (&lt;code&gt;addImport&lt;/code&gt;, &lt;code&gt;formatImports&lt;/code&gt;, &lt;code&gt;getTemplate&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a minimal plugin that reuses the built-in renderer to emit an Entity without &lt;code&gt;class-validator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isEntityHidden&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tommasomeli/prisma-generator-nestjs-dto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BaseGenerator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tommasomeli/prisma-generator-nestjs-dto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tommasomeli/prisma-generator-nestjs-dto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EntityDtoGenerator&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseGenerator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;filePrefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;fileSuffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.entity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;classPrefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;classSuffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredFields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isEntityHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;processedModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filteredFields&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;processedModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;classValidator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputPath&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lifecycle hooks
&lt;/h3&gt;

&lt;p&gt;Plugins can hook the full run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;beforeAll(models)&lt;/code&gt;&lt;/strong&gt; — mutate the shared model list before any generator runs (shared pre-pass)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;afterAll(files)&lt;/code&gt;&lt;/strong&gt; — append barrels, audit reports, or aggregated indexes after all generators finish&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TypeScript plugins, no precompilation
&lt;/h3&gt;

&lt;p&gt;Point &lt;code&gt;extraGenerators&lt;/code&gt; at a &lt;code&gt;.ts&lt;/code&gt; file and the generator loads it via &lt;a href="https://github.com/unjs/jiti" rel="noopener noreferrer"&gt;&lt;code&gt;jiti&lt;/code&gt;&lt;/a&gt;. No build step required for your plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real example: custom &lt;code&gt;@Auditable&lt;/code&gt; annotation
&lt;/h2&gt;

&lt;p&gt;The repo includes a runnable example under &lt;a href="https://github.com/tommasomeli/prisma-generator-nestjs-dto/tree/main/examples/blog" rel="noopener noreferrer"&gt;&lt;code&gt;examples/blog/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema&lt;/strong&gt; — annotate models with a custom &lt;code&gt;@Auditable&lt;/code&gt; name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/// @Auditable("user_audit")
model User {
  id           Int      @id @default(autoincrement())
  email        String   @unique
  /// @DtoHidden
  passwordHash String
  posts        Post[]
}

/// @Auditable("post_audit")
model Post {
  id       Int    @id @default(autoincrement())
  title    String
  author   User   @relation(fields: [authorId], references: [id])
  authorId Int
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Config&lt;/strong&gt; — register the annotation and the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GeneratorConfigFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tommasomeli/prisma-generator-nestjs-dto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;extraAnnotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auditable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;extraGenerators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./generators/audit-generator.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AuditGenerator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;GeneratorConfigFile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; — alongside the usual DTOs, you get per-model audit metadata and an aggregated index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generated/
  user/user.audit.ts
  post/post.audit.ts
  audit-index.ts    ← emitted by AuditGenerator#afterAll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type-safe external config
&lt;/h2&gt;

&lt;p&gt;Prisma's generator block is great for simple flags, but it can't express nested objects or multi-line arrays. For anything richer, use a &lt;code&gt;configFile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generator nestjsDto {
  provider   = "prisma-generator-nestjs-dto"
  output     = "../generated"
  configFile = "../nestjs-dto.config.ts"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;from()&lt;/code&gt; helper validates paths and named exports at &lt;strong&gt;compile time&lt;/strong&gt; (the import closure is never invoked at runtime):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromNamespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GeneratorConfigFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tommasomeli/prisma-generator-nestjs-dto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;extraValidators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/common/validators&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IsUnique&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IsStrongPassword&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;extraDecorators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/common/decorators&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Trim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sanitize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;extraScalars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Decimal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decimal.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MyJson&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;GeneratorConfigFile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then wire annotations in the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
  /// @IsUnique()
  email    String @unique
  /// @IsStrongPassword({ minLength: 10 })
  password String
  /// @Trim()
  name     String
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a custom validator &lt;strong&gt;collides by name&lt;/strong&gt; with a built-in (&lt;code&gt;IsBoolean&lt;/code&gt;, &lt;code&gt;ApiProperty&lt;/code&gt;, …), your module wins for that symbol only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional runtime manifest
&lt;/h2&gt;

&lt;p&gt;Enable &lt;code&gt;emitManifest = "true"&lt;/code&gt; to get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;manifest.ts&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;Record&amp;lt;Prisma.ModelName, { primaryKey, entityFields, relations }&amp;gt;&lt;/code&gt; for select builders, audit middleware, RBAC field lists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;model-entity-map.ts&lt;/code&gt;&lt;/strong&gt; — type-only map from model names to Entity classes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful when you need schema-aware runtime logic without parsing Prisma DMMF yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;This generator&lt;/th&gt;
&lt;th&gt;Typical Prisma NestJS DTO generators&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create / Update / Entity DTOs&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swagger + class-validator&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annotation-driven hide / readonly / type override&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pluggable sub-generators&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom annotations for plugins&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Override built-in imports by name&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-safe external &lt;code&gt;configFile&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional runtime manifest&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; @tommasomeli/prisma-generator-nestjs-dto
npx prisma generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/tommasomeli/prisma-generator-nestjs-dto" rel="noopener noreferrer"&gt;github.com/tommasomeli/prisma-generator-nestjs-dto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@tommasomeli/prisma-generator-nestjs-dto" rel="noopener noreferrer"&gt;@tommasomeli/prisma-generator-nestjs-dto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example:&lt;/strong&gt; &lt;a href="https://github.com/tommasomeli/prisma-generator-nestjs-dto/tree/main/examples/blog" rel="noopener noreferrer"&gt;&lt;code&gt;examples/blog/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues and PRs are welcome — MIT licensed.&lt;/p&gt;

&lt;p&gt;If this saves you time on your NestJS + Prisma stack, a ⭐ on the repo (or a &lt;a href="https://buymeacoffee.com/tommasomeli" rel="noopener noreferrer"&gt;coffee ☕&lt;/a&gt;) goes a long way.&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>nestjs</category>
      <category>openapi</category>
      <category>typescript</category>
    </item>
    <item>
      <title>WordPress plugin boilerplate with React &amp; Vite</title>
      <dc:creator>Tommaso Meli</dc:creator>
      <pubDate>Sat, 23 May 2026 18:03:56 +0000</pubDate>
      <link>https://dev.to/tommasomeli/wordpress-plugin-boilerplate-with-react-vite-2aje</link>
      <guid>https://dev.to/tommasomeli/wordpress-plugin-boilerplate-with-react-vite-2aje</guid>
      <description>&lt;p&gt;Building a WordPress plugin in 2026 still means stitching together half a dozen concerns before you write your first feature: PHP bootstrap, REST routes, asset pipelines, admin UI, i18n, release ZIPs, and (if you're lucky) some kind of dev proxy so you're not manually refreshing minified bundles.&lt;/p&gt;

&lt;p&gt;I've done this enough times for client projects that I finally productized the scaffolding. It's called &lt;strong&gt;&lt;a href="https://github.com/tommasomeli/vipresso" rel="noopener noreferrer"&gt;Vipresso&lt;/a&gt;&lt;/strong&gt; — a GPL boilerplate you fork once, rename once, and ship from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it in the browser (no install):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://playground.wordpress.net/?blueprint-url=https://tommasomeli.it/vipresso/vipresso-blueprint.json" rel="noopener noreferrer"&gt;WordPress Playground demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/tommasomeli/vipresso" rel="noopener noreferrer"&gt;https://github.com/tommasomeli/vipresso&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem I kept hitting
&lt;/h2&gt;

&lt;p&gt;Most "WordPress + React" starters solve one layer well and leave you maintaining registration lists forever:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You add a REST handler → edit a central routes file&lt;/li&gt;
&lt;li&gt;You add an admin tab → wire React Router manually&lt;/li&gt;
&lt;li&gt;You add a shortcode → hope visitors don't download your entire admin bundle&lt;/li&gt;
&lt;li&gt;You cut a release → hand-build a ZIP and pray the headers are correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WordPress already has great primitives (capabilities, options, REST, hooks, cron, WP-CLI). What was missing, for me, was &lt;strong&gt;DX that respects those primitives&lt;/strong&gt; while feeling like a normal Vite + TypeScript app.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Vipresso actually is
&lt;/h2&gt;

&lt;p&gt;Not a plugin you install on a site. A &lt;strong&gt;boilerplate repo&lt;/strong&gt; you fork into your own product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 19 + Vite + TypeScript&lt;/strong&gt; admin SPA with HMR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real WordPress in Docker&lt;/strong&gt; — Vite proxies everything except your bundle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-discovery on both sides&lt;/strong&gt; — drop a file, no manual registration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual bundles&lt;/strong&gt; — &lt;code&gt;app.js&lt;/code&gt; (admin) vs &lt;code&gt;viewer.js&lt;/code&gt; (public shortcodes only)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plop generators&lt;/strong&gt; — routes, REST handlers, widgets, metaboxes, cron, WP-CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-driven settings&lt;/strong&gt; — JSON schema → PHP store + React UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; — lint, Vitest, PHP syntax, Docker + Playwright smoke&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release ZIP + in-plugin auto-updates&lt;/strong&gt; from GitHub Releases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fork → &lt;code&gt;npm run plugin:rename -- YourPlugin&lt;/code&gt; → build features → &lt;code&gt;npm run build:zip&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture: two discovery systems, one repo
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── widgets/          # PHP + TSX + manifest.json
├── shortcodes/       # public-facing, viewer bundle only
├── metaboxes/
├── shared/           # settings.json + generated routes.json
├── src/              # React app (main.tsx + viewer.tsx)
└── plugin/           # WordPress plugin root (Bootstrap, Routes, Assets…)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Vite &lt;code&gt;import.meta.glob&lt;/code&gt; picks up pages, widgets, shortcodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; &lt;code&gt;Bootstrap.php&lt;/code&gt; scans for &lt;code&gt;RouteProvider&lt;/code&gt; classes, cron jobs, WP-CLI commands.&lt;/p&gt;

&lt;p&gt;In dev, feature folders are bind-mounted inside &lt;code&gt;plugin/&lt;/code&gt; so PHP and Node see identical paths. The release script copies them in before zipping.&lt;/p&gt;


&lt;h2&gt;
  
  
  REST without a registration file
&lt;/h2&gt;

&lt;p&gt;Drop a class, implement &lt;code&gt;RouteProvider&lt;/code&gt;, done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BillingHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;\Vipresso\RouteProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'/billing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;\WP_REST_Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;READABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'list'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'/billing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;\WP_REST_Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CREATABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'create'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'amount'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'number'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'currency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'enum'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
            &lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From React: &lt;code&gt;api.get&amp;lt;T&amp;gt;("/billing")&lt;/code&gt; — typed client, namespace from &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keep handlers thin; push business logic into Actions/Services. WordPress coding conventions still apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dual bundles matter
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;src/main.tsx&lt;/code&gt; → admin SPA, dashboard widgets, metaboxes.&lt;br&gt;&lt;br&gt;
&lt;code&gt;src/viewer.tsx&lt;/code&gt; → shortcodes only.&lt;/p&gt;

&lt;p&gt;Public visitors never download admin code. Obvious in hindsight, rarely default in starter templates.&lt;/p&gt;


&lt;h2&gt;
  
  
  Dev workflow
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
pnpm run setup    &lt;span class="c"&gt;# WP install + plugin activation via wp-cli&lt;/span&gt;
pnpm run dev      &lt;span class="c"&gt;# Vite HMR on :3333, proxies WP on :8888&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One &lt;code&gt;.env&lt;/code&gt; drives PHP constants, Vite imports, Docker ports, and &lt;code&gt;plugin.php&lt;/code&gt; / &lt;code&gt;readme.txt&lt;/code&gt; headers (regenerated on every dev/build).&lt;/p&gt;


&lt;h2&gt;
  
  
  Live demo without a server
&lt;/h2&gt;

&lt;p&gt;The Playground blueprint boots throwaway WordPress with the plugin pre-installed and admin auto-login — PHP-WASM in the browser, nothing on your infra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run gen:playground   &lt;span class="c"&gt;# writes wp-playground-blueprint.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publish the blueprint JSON + release ZIP anywhere public; link from your README.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who is this for?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Agency devs shipping custom admin tools on WordPress&lt;/li&gt;
&lt;li&gt;Indie hackers who want WP's ecosystem (users, roles, posts, plugins) with modern frontend DX&lt;/li&gt;
&lt;li&gt;Teams tired of re-scaffolding the same release/REST/i18n plumbing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not for: "I want a SaaS with auth and billing" — this is WordPress-native.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm using Vipresso as the base for client plugins and iterating in public. If you fork it, I'd love to hear what broke or what's missing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issues / feature requests:&lt;/strong&gt; GitHub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions / ideas:&lt;/strong&gt; &lt;a href="https://github.com/tommasomeli/vipresso/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this saves you a week of scaffolding, a ⭐ on the repo helps more than you'd think.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;GPL v2+. Same license as WordPress. Fork it, rename it, ship it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>react</category>
      <category>vite</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Free sshfs GUI for macOS — mount remote folders in Finder</title>
      <dc:creator>Tommaso Meli</dc:creator>
      <pubDate>Sat, 23 May 2026 17:17:17 +0000</pubDate>
      <link>https://dev.to/tommasomeli/free-sshfs-gui-for-macos-mount-remote-folders-in-finder-237a</link>
      <guid>https://dev.to/tommasomeli/free-sshfs-gui-for-macos-mount-remote-folders-in-finder-237a</guid>
      <description>&lt;p&gt;If you manage Linux servers from a Mac, you've probably been here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sshfs user@host:/var/www ~/mnt/prod &lt;span class="nt"&gt;-o&lt;/span&gt; reconnect,auto_cache,noappledouble
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. Until you have four servers, a laptop that sleeps, and a &lt;code&gt;LaunchAgent&lt;/code&gt; you copied from a gist two years ago.&lt;/p&gt;

&lt;p&gt;I wanted &lt;strong&gt;real sshfs mounts in Finder&lt;/strong&gt; — not another SFTP client, not SMB on the VPS — with saved configs and one-click mount/unmount. So I built &lt;strong&gt;SSHFS GUI&lt;/strong&gt;: free, open source, signed macOS DMG.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with sshfs on macOS
&lt;/h2&gt;

&lt;p&gt;Linux makes this easy. On Mac, it's a stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;FUSE implementation&lt;/strong&gt; (FUSE-T or macFUSE)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;&lt;code&gt;sshfs&lt;/code&gt; binary&lt;/strong&gt; (Homebrew core &lt;code&gt;sshfs&lt;/code&gt; is Linux-only — you need &lt;code&gt;fuse-t-sshfs&lt;/code&gt; or &lt;code&gt;gromgit/fuse/sshfs-mac&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;mount command&lt;/strong&gt;, local mount point, reconnect flags, auth&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finder can't mount SSH natively. Tools like Cyberduck are excellent for transfers, but a different workflow than "this remote path is a volume."&lt;/p&gt;

&lt;p&gt;I kept rewriting the same commands. That felt like a small app problem, not a big framework problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SSHFS GUI does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c5dg8oc700d7qx4h5le.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c5dg8oc700d7qx4h5le.png" alt="SSHFS GUI main window" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Save servers&lt;/strong&gt; — host, port, remote path, local mount point, auth method&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click mount/unmount&lt;/strong&gt; — still real &lt;code&gt;sshfs&lt;/code&gt; under the hood&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keychain&lt;/strong&gt; — passwords via &lt;code&gt;keytar&lt;/code&gt;, never in plain JSON&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSH agent, key file, or password&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Favorites &amp;amp; filters&lt;/strong&gt; — all / favorites / mounted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-mount on startup&lt;/strong&gt; — per server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Menu bar mode&lt;/strong&gt; — tray access in packaged builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import/export&lt;/strong&gt; — move configs between Macs as JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EN + IT&lt;/strong&gt; — i18next, system locale detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app also &lt;strong&gt;checks for missing FUSE/sshfs&lt;/strong&gt; and points you to Homebrew install steps if something's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install FUSE + sshfs (once)
&lt;/h2&gt;

&lt;p&gt;Pick one stack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — FUSE-T (no kext, often easiest on recent macOS):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap macos-fuse-t/homebrew-cask
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; fuse-t fuse-t-sshfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B — macFUSE (classic stack):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; macfuse
brew &lt;span class="nb"&gt;install &lt;/span&gt;gromgit/fuse/sshfs-mac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On some FUSE-T setups, Finder needs extra flags (e.g. &lt;code&gt;-o nonamedattr&lt;/code&gt;). You can add those in the app's &lt;strong&gt;SSHFS extra options&lt;/strong&gt; field per server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the app
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Download the signed DMG from &lt;a href="https://github.com/tommasomeli/sshfsgui/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Drag &lt;strong&gt;SSHFS GUI&lt;/strong&gt; to Applications&lt;/li&gt;
&lt;li&gt;Add a server → &lt;strong&gt;Mount&lt;/strong&gt; → open in Finder&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/tommasomeli/sshfsgui" rel="noopener noreferrer"&gt;github.com/tommasomeli/sshfsgui&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;Solo side project, shipped with CI and notarized macOS builds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shell&lt;/td&gt;
&lt;td&gt;Electron 41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;electron-vite + Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React 19, TypeScript, Tailwind, shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Zustand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;keytar → macOS Keychain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;i18next (EN / IT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;Vitest (path handling, import/export, sshfs args)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release&lt;/td&gt;
&lt;td&gt;GitHub Actions → signed + notarized DMG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Interesting bits for fellow Electron devs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Import/export logic&lt;/strong&gt; lives in &lt;code&gt;src/shared/&lt;/code&gt; so Vitest runs on Linux CI without loading &lt;code&gt;keytar&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tray menu&lt;/strong&gt; uses the same i18n keys as the app menu — easy to miss the &lt;code&gt;{ name: appInfo.name }&lt;/code&gt; interpolation on quit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;prebuild&lt;/code&gt; icon script&lt;/strong&gt; skips on non-macOS so CI can &lt;code&gt;electron-vite build&lt;/code&gt; on Ubuntu&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it's not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Not a replacement for Cyberduck or full SFTP workflows&lt;/li&gt;
&lt;li&gt;Not "FUSE-free" — you still need FUSE-T or macFUSE + sshfs, same as CLI&lt;/li&gt;
&lt;li&gt;Not cross-platform today — macOS only&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it / feedback
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/tommasomeli/sshfsgui/releases" rel="noopener noreferrer"&gt;https://github.com/tommasomeli/sshfsgui/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/tommasomeli/sshfsgui" rel="noopener noreferrer"&gt;https://github.com/tommasomeli/sshfsgui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues &amp;amp; feature requests:&lt;/strong&gt; GitHub issue templates welcome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd especially love to hear from anyone running &lt;strong&gt;FUSE-T vs macFUSE on Tahoe&lt;/strong&gt; — mount stability after sleep is the edge case I care about most.&lt;/p&gt;

&lt;p&gt;If you have a cleaner Mac sshfs workflow, tell me in the comments. Always looking to steal good ideas for v1.1.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz971qhunjdm9gco06kfm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz971qhunjdm9gco06kfm.png" alt="SSHFS GUI Logo" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiblri0c6wdpn2l60kmi5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiblri0c6wdpn2l60kmi5.png" alt="SSHFS GUI UI" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16qjylvyn57yov0tkokv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16qjylvyn57yov0tkokv.png" alt="SSHFS GUI settings" width="800" height="623"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>electron</category>
      <category>ssh</category>
      <category>macfuse</category>
    </item>
  </channel>
</rss>
