<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pasi Dev</title><link>https://dev.pasi.pl/</link><description>Recent content on Pasi Dev</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 28 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://dev.pasi.pl/index.xml" rel="self" type="application/rss+xml"/><item><title>About me</title><link>https://dev.pasi.pl/about-me/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://dev.pasi.pl/about-me/</guid><description>&lt;p&gt;I am a &lt;strong&gt;Solutions Architect&lt;/strong&gt; and &lt;strong&gt;Node.js Developer&lt;/strong&gt; and retired PHP developer with over 17 years of experience in total. I also work as a &lt;strong&gt;Full Stack Developer&lt;/strong&gt;, and I handle &lt;strong&gt;DevOps&lt;/strong&gt; to ensure everything I build is production-ready.&lt;/p&gt;
&lt;p&gt;My focus is simple: &lt;strong&gt;Fast delivery without architectural chaos.&lt;/strong&gt; I build systems that are quick to market but remain maintainable and scalable in the long run.&lt;/p&gt;
&lt;h2 id="core-focus"&gt;Core Focus&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Solutions Architect:&lt;/strong&gt; Pragmatic use of DDD, Modular Monoliths, and Hexagonal Architecture where it is needed. No over-engineering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js Developer:&lt;/strong&gt; 10+ years of building high-performance, event-driven backends.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full Stack Developer:&lt;/strong&gt; Building end-to-end applications using &lt;strong&gt;React&lt;/strong&gt; and modern frontend stacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DevOps:&lt;/strong&gt; Automating infrastructure and CI/CD pipelines to keep the ship moving.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I write this blog to show how to deliver high-quality software without getting lost in over-engineering and redundant patterns.&lt;/p&gt;</description></item><item><title>Simplified monorepo with docker compose for local development, CI and production</title><link>https://dev.pasi.pl/simplified-monorepo-with-docker-compose-for-local-development-ci-and-production/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://dev.pasi.pl/simplified-monorepo-with-docker-compose-for-local-development-ci-and-production/</guid><description>&lt;p&gt;Docker Compose is often treated as a &amp;ldquo;local-only&amp;rdquo; tool, while CI pipelines reimplement the same logic with custom scripts and duplicated image definitions. That duplication causes drift, surprises, and unnecessary complexity.&lt;/p&gt;
&lt;p&gt;Using &lt;strong&gt;multi-stage Dockerfiles&lt;/strong&gt; combined with &lt;strong&gt;build targets selected via environment variables&lt;/strong&gt;, Docker Compose can become the single source of truth for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;local development&lt;/li&gt;
&lt;li&gt;production-like local testing&lt;/li&gt;
&lt;li&gt;CI build&lt;/li&gt;
&lt;li&gt;CI push&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No separate compose files. No duplicated CI job definitions per image.&lt;/p&gt;</description></item><item><title>Rethinking Date Handling in DTOs: Why Strings Make More Sense in NodeJS</title><link>https://dev.pasi.pl/rethinking-date-handling-in-dtos-why-strings-make-more-sense-in-nodejs/</link><pubDate>Tue, 07 Oct 2025 00:00:00 +0000</pubDate><guid>https://dev.pasi.pl/rethinking-date-handling-in-dtos-why-strings-make-more-sense-in-nodejs/</guid><description>&lt;p&gt;Data Transfer Objects (DTOs) are the contracts between client and the server. They need to be &lt;strong&gt;simple&lt;/strong&gt;, &lt;strong&gt;serializable&lt;/strong&gt;, and &lt;strong&gt;explicit&lt;/strong&gt;. Yet over the years NodeJS developers casually drop JavaScript &lt;code&gt;Date&lt;/code&gt; objects into DTOs—thinking it&amp;rsquo;s harmless and convenient.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;Date&lt;/code&gt; in DTOs introduces subtle bugs, timezone inconsistencies, test fragility, and serialization mismatches. It also violates the basic principle of DTO design: &lt;strong&gt;no hidden behavior&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s why you should stick to ISO 8601 strings instead—and what can go wrong if you don&amp;rsquo;t.&lt;/p&gt;</description></item><item><title>Introducing the 3.1 Maturity Level of REST API Design: JSON Schema-Aware Hypermedia</title><link>https://dev.pasi.pl/introducing-the-3-1-maturity-level-of-rest-api-design-json-schema-aware-hypermedia/</link><pubDate>Fri, 13 Jun 2025 00:00:00 +0000</pubDate><guid>https://dev.pasi.pl/introducing-the-3-1-maturity-level-of-rest-api-design-json-schema-aware-hypermedia/</guid><description>&lt;p&gt;REST API maturity is often evaluated using the Richardson Maturity Model (RMM), which defines four levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Level 0&lt;/strong&gt; – Single URI, typically using POST for everything&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 1&lt;/strong&gt; – Resources identified by URIs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 2&lt;/strong&gt; – Use of HTTP methods to define behavior (GET, POST, PUT, DELETE)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 3&lt;/strong&gt; – Hypermedia controls (HATEOAS)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, traditional REST maturity models often overlook an important aspect: schema awareness. That&amp;rsquo;s why I would like to propose a new, unofficial but pragmatic step after Level 3:&lt;/p&gt;</description></item><item><title>Hexagonal Architecture Abuse: When Organization Becomes Obfuscation</title><link>https://dev.pasi.pl/hexagonal-architecture-abuse-when-organization-becomes-obfuscation/</link><pubDate>Wed, 11 Jun 2025 00:00:00 +0000</pubDate><guid>https://dev.pasi.pl/hexagonal-architecture-abuse-when-organization-becomes-obfuscation/</guid><description>&lt;p&gt;Hexagonal architecture (also known as Ports and Adapters) is a powerful way to design software that is independent of frameworks, databases, or UI layers. Its main goal is to isolate the &lt;strong&gt;core business logic&lt;/strong&gt; from the outside world by enforcing clear boundaries. However, in many projects, this pattern is over-engineered to the point where the supposed simplicity turns into a maze of indirection, deeply nested folders, and needless abstractions.&lt;/p&gt;
&lt;h2 id="the-problem-overstructuring-a-simple-concept"&gt;The Problem: Overstructuring a Simple Concept&lt;/h2&gt;
&lt;p&gt;Many developers (including me) start with the intent of using hexagonal architecture but quickly spiral into the following:&lt;/p&gt;</description></item><item><title>Misusing Command and Query Buses in Modular Monoliths: A NestJS Perspective</title><link>https://dev.pasi.pl/misusing-command-and-query-buses-in-modular-monoliths-a-nestjs-perspective/</link><pubDate>Mon, 09 Jun 2025 00:00:00 +0000</pubDate><guid>https://dev.pasi.pl/misusing-command-and-query-buses-in-modular-monoliths-a-nestjs-perspective/</guid><description>&lt;p&gt;In recent years, architectural patterns like &lt;strong&gt;Command Bus&lt;/strong&gt; and &lt;strong&gt;Query Bus&lt;/strong&gt; have gained popularity, especially in TypeScript backends using NestJS and its &lt;code&gt;@nestjs/cqrs&lt;/code&gt; package. While these patterns offer separation of concerns, they are often overused in places where a simple, clean application service or facade would be better.&lt;/p&gt;
&lt;h2 id="the-problem-overengineering-in-a-modular-monolith"&gt;The Problem: Overengineering in a Modular Monolith&lt;/h2&gt;
&lt;p&gt;Consider this common anti-pattern in NestJS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// update-profile.command.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;export&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; UpdateProfileCommand {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;constructor&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; userId: &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt;, &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; nickname: &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt;) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// update-profile.handler.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;@CommandHandler&lt;/span&gt;(UpdateProfileCommand)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;export&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; UpdateProfileHandler &lt;span style="color:#ff7b72"&gt;implements&lt;/span&gt; ICommandHandler {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;constructor&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;private&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; userRepository: &lt;span style="color:#ff7b72"&gt;UserRepository&lt;/span&gt;) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;async&lt;/span&gt; execute(command: &lt;span style="color:#ff7b72"&gt;UpdateProfileCommand&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;const&lt;/span&gt; user &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;await&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;this&lt;/span&gt;.userRepository.findById(command.userId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user.updateNickname(command.nickname);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;await&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;this&lt;/span&gt;.userRepository.save(user);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// get-profile.query.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;export&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; GetProfileQuery {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;constructor&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; userId: &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt;) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// get-profile.handler.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;@QueryHandler&lt;/span&gt;(GetProfileQuery)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;export&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; GetProfileHandler &lt;span style="color:#ff7b72"&gt;implements&lt;/span&gt; IQueryHandler {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;constructor&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;private&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; userRepository: &lt;span style="color:#ff7b72"&gt;UserRepository&lt;/span&gt;) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;async&lt;/span&gt; execute(query: &lt;span style="color:#ff7b72"&gt;GetProfileQuery&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;const&lt;/span&gt; user &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;await&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;this&lt;/span&gt;.userRepository.findById(query.userId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; { userId: &lt;span style="color:#ff7b72"&gt;user.id&lt;/span&gt;, nickname: &lt;span style="color:#ff7b72"&gt;user.nickname&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// user-profile.controller.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;await&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;this&lt;/span&gt;.commandBus.execute(&lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; UpdateProfileCommand(userId, nickname));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;await&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;this&lt;/span&gt;.queryBus.execute(&lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; GetProfileQuery(userId));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s &lt;strong&gt;four classes&lt;/strong&gt; and &lt;strong&gt;four files&lt;/strong&gt; just to write 2 methods. Where is the value in that? Where is the gain?&lt;/p&gt;</description></item></channel></rss>