<?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: luis-botelho</title>
    <description>The latest articles on DEV Community by luis-botelho (@luisbotelho).</description>
    <link>https://dev.to/luisbotelho</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F677311%2F40a090ea-86ca-4b5d-ac7a-53e5d53c3420.jpeg</url>
      <title>DEV Community: luis-botelho</title>
      <link>https://dev.to/luisbotelho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luisbotelho"/>
    <language>en</language>
    <item>
      <title>Building LIA (Part 1 Implementation): Clean Architecture and Argon2id in a Real Fastify + Prisma Registration Flow</title>
      <dc:creator>luis-botelho</dc:creator>
      <pubDate>Wed, 15 Jul 2026 03:54:06 +0000</pubDate>
      <link>https://dev.to/luisbotelho/building-lia-part-1-implementation-clean-architecture-and-argon2id-in-a-real-fastify-prisma-17jh</link>
      <guid>https://dev.to/luisbotelho/building-lia-part-1-implementation-clean-architecture-and-argon2id-in-a-real-fastify-prisma-17jh</guid>
      <description>&lt;p&gt;LIA is a hyperlocal employability platform I'm building for an isolated coastal district in Brazil — think fixed retail jobs, gigs, and a reputation layer, all matched by proximity instead of routed through a national job board.&lt;/p&gt;

&lt;p&gt;This post is about the implementation: the actual folder structure, the real RegisterUserUseCase, and the Argon2id decision — pulled straight from the repository, not reconstructed from memory.&lt;/p&gt;

&lt;p&gt;The Clean Architecture folder structure&lt;/p&gt;

&lt;p&gt;LIA's backend is organized in four layers, and the direction of dependency is non-negotiable: outer layers depend on inner layers, never the other way around.&lt;/p&gt;

&lt;p&gt;backend/src/&lt;br&gt;
├── domain/&lt;br&gt;
│   ├── entities/&lt;br&gt;
│   └── repositories/       # interfaces only&lt;br&gt;
├── application/&lt;br&gt;
│   ├── dto/&lt;br&gt;
│   └── use-cases/&lt;br&gt;
├── infrastructure/&lt;br&gt;
│   ├── database/&lt;br&gt;
│   └── repositories/       # Prisma implementations&lt;br&gt;
├── presentation/&lt;br&gt;
│   ├── controllers/&lt;br&gt;
│   └── routes/&lt;br&gt;
└── shared/&lt;br&gt;
    └── errors/&lt;/p&gt;

&lt;p&gt;Let's walk through the registration feature end to end, following that exact order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Domain — the entity and the repository contract&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The User entity is a plain interface. No decorators, no ORM annotations, no framework leaking in:&lt;/p&gt;

&lt;p&gt;typescript// domain/entities/user.ts&lt;br&gt;
export interface User {&lt;br&gt;
  id: string;&lt;br&gt;
  name: string;&lt;br&gt;
  email: string;&lt;br&gt;
  password: string;&lt;br&gt;
  createdAt: Date;&lt;br&gt;
  updatedAt: Date;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The repository is defined as a contract, not an implementation. The domain doesn't know — and doesn't care — whether it's backed by PostgreSQL, an in-memory map, or something else entirely:&lt;/p&gt;

&lt;p&gt;typescript// domain/repositories/user.repository.ts&lt;br&gt;
import { RegisterUserDTO } from '../../application/dto/register-user.dto.js';&lt;/p&gt;

&lt;p&gt;export interface UserRepository {&lt;br&gt;
  create(data: RegisterUserDTO): Promise&amp;lt;{&lt;br&gt;
    id: string;&lt;br&gt;
    name: string;&lt;br&gt;
    email: string;&lt;br&gt;
    createdAt: Date;&lt;br&gt;
    updatedAt: Date;&lt;br&gt;
  }&amp;gt;;&lt;/p&gt;

&lt;p&gt;findByEmail(email: string): Promise&amp;lt;{&lt;br&gt;
    id: string;&lt;br&gt;
    name: string;&lt;br&gt;
    email: string;&lt;br&gt;
    password: string;&lt;br&gt;
    createdAt: Date;&lt;br&gt;
    updatedAt: Date;&lt;br&gt;
  } | null&amp;gt;;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Notice create() never returns the password hash. That's not an accident — it's the same "strip credentials from every response payload" rule from the code review checklist in Part 1, enforced at the type level before it can ever leak.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Application — the use case&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where the actual business rule lives: validate input, enforce uniqueness, hash the password, delegate persistence.&lt;/p&gt;

&lt;p&gt;typescript// application/use-cases/register-user.use-case.ts&lt;br&gt;
import { RegisterUserDTO } from '../dto/register-user.dto.js';&lt;br&gt;
import { PrismaUserRepository } from '../../infrastructure/repositories/prisma-user.repository.js';&lt;br&gt;
import argon2 from 'argon2';&lt;br&gt;
import { AppError } from '../../shared/errors/app-error.js';&lt;/p&gt;

&lt;p&gt;export class RegisterUserUseCase {&lt;br&gt;
  private readonly repository = new PrismaUserRepository();&lt;/p&gt;

&lt;p&gt;async execute(data: RegisterUserDTO) {&lt;br&gt;
    if (!data.name.trim()) {&lt;br&gt;
      throw new AppError('Nome é obrigatório.', 400);&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (!data.email.trim()) {
  throw new AppError('E-mail é obrigatório.', 400);
}

if (!data.password.trim()) {
  throw new AppError('Senha é obrigatória.', 400);
}
const existingUser = await this.repository.findByEmail(data.email);

if (existingUser) {
  throw new AppError('E-mail já cadastrado.', 409);
}
const hashedPassword = await argon2.hash(data.password);

return this.repository.create({ ...data, password: hashedPassword });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;A few things worth calling out for readers coming from an Express/MVC background:&lt;/p&gt;

&lt;p&gt;AppError is a custom error class carrying an HTTP status code, but it's thrown from the application layer with zero knowledge of HTTP itself. The presentation layer decides how to translate it into a response — the use case just says "this is a 409."&lt;br&gt;
Argon2id is called with library defaults here — argon2.hash(data.password). That's a legitimate MVP trade-off, and one this codebase is explicit about: [ADR-013] documents Argon2id as the algorithm decision (chosen over bcrypt for GPU-attack resistance per current OWASP guidance), while parameter tuning (memory cost, iterations) is left as a follow-up, not silently skipped.&lt;br&gt;
Validation is intentionally basic (non-empty checks) at this stage of the project — this is the foundation sprint, not the final input-validation layer. Being upfront about that is part of the same "document the why" discipline from Part 1.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Infrastructure — the Prisma implementation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository interface gets implemented here, and only here. This is the piece that would get replaced if LIA ever moved off Prisma or off PostgreSQL — nothing above this layer would need to change.&lt;/p&gt;

&lt;p&gt;typescript// infrastructure/repositories/prisma-user.repository.ts&lt;br&gt;
import { UserRepository } from '../../domain/repositories/user.repository.js';&lt;br&gt;
import { RegisterUserDTO } from '../../application/dto/register-user.dto.js';&lt;br&gt;
import { prisma } from '../database/prisma.js';&lt;/p&gt;

&lt;p&gt;export class PrismaUserRepository implements UserRepository {&lt;br&gt;
  async findByEmail(email: string) {&lt;br&gt;
    return prisma.user.findUnique({&lt;br&gt;
      where: {&lt;br&gt;
        email,&lt;br&gt;
      },&lt;br&gt;
    });&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;async create(data: RegisterUserDTO) {&lt;br&gt;
    return prisma.user.create({&lt;br&gt;
      data,&lt;br&gt;
      select: {&lt;br&gt;
        id: true,&lt;br&gt;
        name: true,&lt;br&gt;
        email: true,&lt;br&gt;
        createdAt: true,&lt;br&gt;
        updatedAt: true,&lt;br&gt;
      },&lt;br&gt;
    });&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;That select clause on create() is doing real work: it's a second, infrastructure-level guarantee that the password hash never makes it into the returned object — even if someone changes the domain contract upstream later.&lt;/p&gt;

&lt;p&gt;Prisma itself is wired up through the driver adapter pattern introduced in Prisma 7, connecting directly against pg:&lt;/p&gt;

&lt;p&gt;typescript// infrastructure/database/prisma.ts&lt;br&gt;
import 'dotenv/config';&lt;br&gt;
import { PrismaClient } from '../../generated/prisma/client.js';&lt;br&gt;
import { PrismaPg } from '@prisma/adapter-pg';&lt;/p&gt;

&lt;p&gt;const adapter = new PrismaPg({&lt;br&gt;
  connectionString: process.env.DATABASE_URL!,&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;export const prisma = new PrismaClient({ adapter });&lt;/p&gt;

&lt;p&gt;This is a small but real example of what an ADR buys you in practice. [ADR-011] exists specifically because Prisma 7 changed its configuration conventions from earlier versions — without that ADR on record, a future "helpful" refactor could silently reintroduce a deprecated config pattern.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Presentation — controller, routes, and the server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The controller's only job is translating HTTP in and out. It has no business logic — it just calls the use case and maps AppError to a response:&lt;/p&gt;

&lt;p&gt;typescript// presentation/controllers/register.controller.ts&lt;br&gt;
import { FastifyReply, FastifyRequest } from 'fastify';&lt;br&gt;
import { RegisterUserUseCase } from '../../application/use-cases/register-user.use-case.js';&lt;br&gt;
import { AppError } from '../../shared/errors/app-error.js';&lt;br&gt;
import { RegisterUserDTO } from '../../application/dto/register-user.dto.js';&lt;/p&gt;

&lt;p&gt;export class RegisterController {&lt;br&gt;
  constructor(private readonly registerUserUseCase: RegisterUserUseCase) {}&lt;/p&gt;

&lt;p&gt;async handle(request: FastifyRequest, reply: FastifyReply) {&lt;br&gt;
    try {&lt;br&gt;
      const body = request.body as RegisterUserDTO;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  const user = await this.registerUserUseCase.execute(body);

  return reply.status(201).send(user);
} catch (error) {
  if (error instanceof AppError) {
    return reply.status(error.statusCode).send({
      message: error.message,
    });
  }

  request.log.error(error);

  return reply.status(500).send({
    message: 'Internal Server Error',
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The use case is constructor-injected rather than instantiated inline inside handle() — that's what makes it possible to swap it for a mock in a controller test without touching Fastify at all.&lt;/p&gt;

&lt;p&gt;Routing wires the controller to Fastify with the bare minimum of ceremony:&lt;/p&gt;

&lt;p&gt;typescript// presentation/routes/register.routes.ts&lt;br&gt;
import { FastifyInstance } from 'fastify';&lt;br&gt;
import { RegisterController } from '../controllers/register.controller.js';&lt;br&gt;
import { RegisterUserUseCase } from '../../application/use-cases/register-user.use-case.js';&lt;/p&gt;

&lt;p&gt;const registerUserUseCase = new RegisterUserUseCase();&lt;br&gt;
const controller = new RegisterController(registerUserUseCase);&lt;/p&gt;

&lt;p&gt;export async function registerRoutes(app: FastifyInstance) {&lt;br&gt;
  app.post('/register', controller.handle.bind(controller));&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;And the Fastify instance itself — [ADR-012] — stays intentionally thin at this stage: CORS, a health check, and the registration routes:&lt;/p&gt;

&lt;p&gt;typescript// presentation/server.ts&lt;br&gt;
import Fastify from 'fastify';&lt;br&gt;
import cors from '@fastify/cors';&lt;br&gt;
import { registerRoutes } from './routes/register.routes.js';&lt;/p&gt;

&lt;p&gt;export const app = Fastify({&lt;br&gt;
  logger: true,&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;await app.register(cors, {&lt;br&gt;
  origin: true, // dev only — opens CORS to any origin, e.g. localhost:5173&lt;br&gt;
  methods: ['GET', 'POST', 'PUT', 'DELETE'],&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;app.get('/health', async () =&amp;gt; ({&lt;br&gt;
  status: 'ok',&lt;br&gt;
}));&lt;/p&gt;

&lt;p&gt;app.register(registerRoutes);&lt;/p&gt;

&lt;p&gt;The shared error class tying it together&lt;/p&gt;

&lt;p&gt;One small class does a lot of quiet work across every layer above:&lt;/p&gt;

&lt;p&gt;typescript// shared/errors/app-error.ts&lt;br&gt;
export class AppError extends Error {&lt;br&gt;
  constructor(&lt;br&gt;
    message: string,&lt;br&gt;
    public readonly statusCode = 400,&lt;br&gt;
  ) {&lt;br&gt;
    super(message);&lt;br&gt;
    this.name = 'AppError';&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;It's what lets the use case say "this is a business rule violation, and here's the HTTP status that should map to it" without importing anything HTTP-related. The presentation layer is the only place that ever turns that status code into an actual response.&lt;/p&gt;

&lt;p&gt;Why this is worth doing on a solo project&lt;/p&gt;

&lt;p&gt;None of this is exotic engineering — it's the kind of layering most backend teams settle into eventually. The point of doing it explicitly, from the first registration endpoint, on a one-person project is that it turns architectural discipline into something enforceable instead of aspirational. The Definition of Done checklist behind this feature — password hashing validated at the persistence layer, credentials stripped from every response, centralized error handling — isn't a wishlist here. You can grep for it in this code and find it already true.&lt;/p&gt;

&lt;p&gt;Questions about the trade-offs above — especially the Argon2id defaults or the validation approach — are welcome in the comments. This is an active build, and Part 2 will cover the frontend registration flow and where the current implementation gets hardened next.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>cleancode</category>
      <category>backend</category>
    </item>
    <item>
      <title>React + MVVM: How I'm Organizing My Frontend for Scalability</title>
      <dc:creator>luis-botelho</dc:creator>
      <pubDate>Mon, 13 Jul 2026 05:14:31 +0000</pubDate>
      <link>https://dev.to/luisbotelho/react-mvvm-how-im-organizing-my-frontend-for-scalability-4lo1</link>
      <guid>https://dev.to/luisbotelho/react-mvvm-how-im-organizing-my-frontend-for-scalability-4lo1</guid>
      <description>&lt;h2&gt;
  
  
  The Hook
&lt;/h2&gt;

&lt;p&gt;Have you ever felt like your React components are doing &lt;em&gt;too much&lt;/em&gt;? 😅 Fetching data, managing loading/error state, formatting it, AND rendering the UI — all in one file.&lt;/p&gt;

&lt;p&gt;In my project, &lt;strong&gt;SafeAnchor&lt;/strong&gt; (a vertical SaaS for maritime fleet management), I decided to move away from "vibe coding" and commit to a proper architecture: &lt;strong&gt;MVVM (Model-View-ViewModel)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;![SafeAnchor MVVM data flow: View, ViewModel, Service and API]&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkgi7ev29q9mnh3wxcqx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkgi7ev29q9mnh3wxcqx.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why MVVM?
&lt;/h2&gt;

&lt;p&gt;For a junior developer in 2026, understanding system architecture — not just syntax — is a must-have skill. Here's how I decoupled my frontend logic into three layers, straight from &lt;code&gt;apps/frontend/src/&lt;/code&gt; in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── views/         # screens (JSX)
├── viewmodels/    # screen state + logic (custom hooks)
├── services/      # API communication
└── models/        # data shape
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Services — pure API logic
&lt;/h3&gt;

&lt;p&gt;Services don't care about buttons or spinners. They only care about talking to the API and shaping the response. This is the actual &lt;code&gt;getVesselById&lt;/code&gt; from &lt;code&gt;services/vesselService.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getVesselById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/vessels/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Nao foi possivel carregar a embarcação.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;vessel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;vessel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fallbackVessels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;vessel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fallback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&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="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;Look closely at the &lt;code&gt;catch&lt;/code&gt;: if the request fails, the service returns a hardcoded local vessel instead of throwing. That's a deliberate resilience decision — more on it below.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hooks as ViewModels — the brain
&lt;/h3&gt;

&lt;p&gt;All state management — loading, fetching, error handling — lives here. This is what keeps the UI clean. From &lt;code&gt;viewmodels/useVesselDetailsViewModel.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useVesselDetailsViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;vessel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setVessel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadVessel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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;loaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getVesselById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;setVessel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vessel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Não foi possível carregar a embarcação.&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="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setIsLoading&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;loadVessel&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;id&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="nx"&gt;vessel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;removeVessel&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;
  
  
  3. Clean Views — just props in, JSX out
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;views/VesselDetailsPage.jsx&lt;/code&gt; only receives data via the hook. No fetching, no logic — which makes it much faster to test and maintain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VesselDetailsPage&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useParams&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;viewModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useVesselDetailsViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Carregando...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vessel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vessel&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"vessel-details"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"vessel-details__title"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Embarcação&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"vessel-details__value"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vessel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Put together, the data flows one way: &lt;strong&gt;API → Service → ViewModel (Hook) → View (JSX)&lt;/strong&gt; — no shortcuts, no components secretly reaching into the network layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fallback That Made Me Rethink "MVP Code"
&lt;/h2&gt;

&lt;p&gt;![Resilience fallback flow when the backend is offline]&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd2r0zftkhtnjjrn2cc9d.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd2r0zftkhtnjjrn2cc9d.png" alt=" " width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The nicest surprise re-reading my own code: &lt;code&gt;vesselService.js&lt;/code&gt; already falls back to an in-memory vessel array when the Express backend is unreachable. The ViewModel and the View don't know (or care) which path ran — they just get &lt;code&gt;{ vessel, source, error }&lt;/code&gt;. One &lt;code&gt;try/catch&lt;/code&gt; buys the whole app the ability to stay demoable even with the backend down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where SafeAnchor Actually Stands
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Epic 001 — Fleet Management Foundation&lt;/strong&gt;: vessel listing, details, creation, editing, archiving — all shipped.&lt;/li&gt;
&lt;li&gt;🔜 &lt;strong&gt;Epic 002 — Maintenance Management&lt;/strong&gt;: up next.&lt;/li&gt;
&lt;li&gt;🗺️ Later: Safety Checklists (with QR-code field access), Document Management, Maritime Academy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Tech Stack &amp;amp; Learning Process
&lt;/h2&gt;

&lt;p&gt;Right now it's &lt;strong&gt;React + Vite&lt;/strong&gt; on the frontend and &lt;strong&gt;Express&lt;/strong&gt; on the backend, with a documented target migration to &lt;strong&gt;Next.js + TypeScript + Redux Toolkit&lt;/strong&gt; as the app grows into a feature-based structure. To speed up my learning, I use &lt;strong&gt;NotebookLM&lt;/strong&gt; to organize scattered concepts into structured technical guides — basically an AI-powered tutor for engineering fundamentals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Refactoring CSS to the &lt;strong&gt;BEM&lt;/strong&gt; standard&lt;/li&gt;
&lt;li&gt;Hardening the fallback/error paths ahead of Maintenance Management&lt;/li&gt;
&lt;li&gt;Building out Safety Checklists and QR-code field access&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Let's Discuss! 👇
&lt;/h2&gt;

&lt;p&gt;How do you handle state logic in your React apps? Do you prefer big all-in-one hooks, or a decentralized architecture like MVVM? Let me know in the comments!&lt;/p&gt;

&lt;h1&gt;
  
  
  SafeAnchor #WebDev #React #Architecture #LearningInPublic #SoftwareEngineering
&lt;/h1&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
