Código e Automação

7 de mai. de 2026

Go back

Meu Mercado: construindo uma lista de compras full-stack com React, Express e SQLite

Autor: Rafael Lins

Veja como foi construído o Meu Mercado, uma aplicação full-stack com React, Express, SQLite e Docker focada em listas de compras mobile-first e arquitetura pragmática.

Create a highly detailed anime-style illustrated scene with manga art direction of a bustling supermarket at lunchtime

Fique por dentro do que há de mais relavante no Marketing Digital, assine a nossa newsletter:

O Meu Mercado nasceu como uma aplicação simples na superfície: criar e acompanhar listas de compras mensais. Mas, por baixo da interface, o projeto resolve vários problemas clássicos de uma aplicação full-stack pequena e pronta para produção: autenticação, persistência local, seed de dados, API REST, estado no frontend, build estático, base path em subdiretório e deploy via Docker.

A ideia principal é transformar um processo doméstico recorrente em um sistema estruturado. Em vez de montar uma lista do zero todo mês, o usuário trabalha com um catálogo pré-cadastrado de produtos, quantidades ideais, categorias e histórico de listas. A aplicação permite criar listas, clonar compras anteriores, marcar itens como comprados, ajustar quantidades e acompanhar o progresso em tempo real.

Tela 3 do sistema

Stack

O projeto usa uma stack enxuta, escolhida para reduzir complexidade operacional:

  • React 18 para a interface

  • TypeScript no frontend para tipar entidades e contratos

  • Vite como ferramenta de desenvolvimento e build

  • Tailwind CSS para a camada visual

  • Lucide React para iconografia

  • Node.js + Express no backend

  • SQLite como banco embarcado

  • better-sqlite3 como driver síncrono e performático para SQLite

  • bcryptjs para hash de senha

  • JWT para autenticação stateless

  • Docker para empacotamento e deploy

  • Nginx como reverse proxy em produção

Essa combinação funciona muito bem para aplicações com baixo custo de infraestrutura, poucos pontos móveis e necessidade de persistência confiável sem provisionar um servidor de banco separado.

Arquitetura geral

A aplicação é dividida em duas partes:

Frontend React/Vite
  |
  | HTTP + JWT
  v
Backend Express
  |
  | better-sqlite3
  v
SQLite
Frontend React/Vite
  |
  | HTTP + JWT
  v
Backend Express
  |
  | better-sqlite3
  v
SQLite
Frontend React/Vite
  |
  | HTTP + JWT
  v
Backend Express
  |
  | better-sqlite3
  v
SQLite

Em desenvolvimento, o Vite roda o frontend e usa proxy para encaminhar chamadas de API para o Express:

server: {
  proxy: {
    '/meu-mercado/api': {
      target: 'http://localhost:3001',
      changeOrigin: true,
    },
  },
}
server: {
  proxy: {
    '/meu-mercado/api': {
      target: 'http://localhost:3001',
      changeOrigin: true,
    },
  },
}
server: {
  proxy: {
    '/meu-mercado/api': {
      target: 'http://localhost:3001',
      changeOrigin: true,
    },
  },
}

Em produção, o Express também serve os arquivos estáticos gerados pelo Vite:

const distPath = path.join(__dirname, '..', 'dist');
app.use(`${BASE_PATH}`, express.static(distPath));
const distPath = path.join(__dirname, '..', 'dist');
app.use(`${BASE_PATH}`, express.static(distPath));
const distPath = path.join(__dirname, '..', 'dist');
app.use(`${BASE_PATH}`, express.static(distPath));

O mesmo servidor Node entrega o frontend e responde a API. Isso simplifica bastante o deploy: um único container sobe a aplicação inteira.

Base path em subdiretório

Um detalhe importante é que o projeto foi desenhado para rodar em:

/meu-mercado/
/meu-mercado/
/meu-mercado/

Isso aparece tanto no Vite quanto no Express:

base: '/meu-mercado/'
base: '/meu-mercado/'
base: '/meu-mercado/'
const BASE_PATH = '/meu-mercado';
const BASE_PATH = '/meu-mercado';
const BASE_PATH = '/meu-mercado';

Essa decisão evita problemas quando a aplicação não está na raiz do domínio, por exemplo em:

https://dominio.com.br/meu-mercado/
https://dominio.com.br/meu-mercado/
https://dominio.com.br/meu-mercado/

Sem esse cuidado, assets, rotas e chamadas de API poderiam quebrar em produção, especialmente atrás de um reverse proxy.

Modelo de dados

O banco usa SQLite e é inicializado automaticamente na primeira execução. A conexão é aberta com duas configurações importantes:

db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

O modo WAL melhora o comportamento de leitura e escrita concorrente no SQLite. Já foreign_keys = ON garante que as relações declaradas no schema sejam respeitadas.

O modelo possui cinco tabelas principais:

users
categories
items
shopping_lists
shopping_list_items
users
categories
items
shopping_lists
shopping_list_items
users
categories
items
shopping_lists
shopping_list_items

A tabela users guarda o login:

CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TEXT DEFAULT (datetime('now'))
)

CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TEXT DEFAULT (datetime('now'))
)

CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TEXT DEFAULT (datetime('now'))
)

As categorias agrupam os produtos:

CREATE TABLE IF NOT EXISTS categories (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE NOT NULL,
  icon TEXT NOT NULL DEFAULT 'package',
  sort_order INTEGER NOT NULL DEFAULT 0
)

CREATE TABLE IF NOT EXISTS categories (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE NOT NULL,
  icon TEXT NOT NULL DEFAULT 'package',
  sort_order INTEGER NOT NULL DEFAULT 0
)

CREATE TABLE IF NOT EXISTS categories (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE NOT NULL,
  icon TEXT NOT NULL DEFAULT 'package',
  sort_order INTEGER NOT NULL DEFAULT 0
)

Tela categorias do sistema

Os itens representam o catálogo base da aplicação:

CREATE TABLE IF NOT EXISTS items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  category_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  ideal_qty REAL DEFAULT 0,
  unit TEXT DEFAULT 'un',
  perishable INTEGER DEFAULT 0,
  purchase_freq TEXT DEFAULT 'Estoque',
  notes TEXT DEFAULT '',
  sort_order INTEGER DEFAULT 0,
  FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
)

CREATE TABLE IF NOT EXISTS items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  category_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  ideal_qty REAL DEFAULT 0,
  unit TEXT DEFAULT 'un',
  perishable INTEGER DEFAULT 0,
  purchase_freq TEXT DEFAULT 'Estoque',
  notes TEXT DEFAULT '',
  sort_order INTEGER DEFAULT 0,
  FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
)

CREATE TABLE IF NOT EXISTS items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  category_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  ideal_qty REAL DEFAULT 0,
  unit TEXT DEFAULT 'un',
  perishable INTEGER DEFAULT 0,
  purchase_freq TEXT DEFAULT 'Estoque',
  notes TEXT DEFAULT '',
  sort_order INTEGER DEFAULT 0,
  FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
)

As listas pertencem a um usuário e possuem estado:

CREATE TABLE IF NOT EXISTS shopping_lists (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  date TEXT NOT NULL,
  status TEXT DEFAULT 'draft' CHECK(status IN ('draft','active','completed')),
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)

CREATE TABLE IF NOT EXISTS shopping_lists (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  date TEXT NOT NULL,
  status TEXT DEFAULT 'draft' CHECK(status IN ('draft','active','completed')),
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)

CREATE TABLE IF NOT EXISTS shopping_lists (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  date TEXT NOT NULL,
  status TEXT DEFAULT 'draft' CHECK(status IN ('draft','active','completed')),
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)

Por fim, shopping_list_items resolve a relação entre uma lista e os itens do catálogo:

CREATE TABLE IF NOT EXISTS shopping_list_items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  list_id INTEGER NOT NULL,
  item_id INTEGER NOT NULL,
  qty REAL NOT NULL DEFAULT 1,
  purchased INTEGER DEFAULT 0,
  purchased_at TEXT,
  FOREIGN KEY (list_id) REFERENCES shopping_lists(id) ON DELETE CASCADE,
  FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
)

CREATE TABLE IF NOT EXISTS shopping_list_items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  list_id INTEGER NOT NULL,
  item_id INTEGER NOT NULL,
  qty REAL NOT NULL DEFAULT 1,
  purchased INTEGER DEFAULT 0,
  purchased_at TEXT,
  FOREIGN KEY (list_id) REFERENCES shopping_lists(id) ON DELETE CASCADE,
  FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
)

CREATE TABLE IF NOT EXISTS shopping_list_items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  list_id INTEGER NOT NULL,
  item_id INTEGER NOT NULL,
  qty REAL NOT NULL DEFAULT 1,
  purchased INTEGER DEFAULT 0,
  purchased_at TEXT,
  FOREIGN KEY (list_id) REFERENCES shopping_lists(id) ON DELETE CASCADE,
  FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
)

Tela itens do sistema

Essa separação é importante porque o item do catálogo continua sendo uma referência global, enquanto a quantidade e o status de compra são específicos de cada lista.

Seed automático

Na primeira execução, o backend verifica se já existem categorias:

const catCount = db.prepare('SELECT COUNT(*) as c FROM categories').get().c;
if (catCount > 0) return;
const catCount = db.prepare('SELECT COUNT(*) as c FROM categories').get().c;
if (catCount > 0) return;
const catCount = db.prepare('SELECT COUNT(*) as c FROM categories').get().c;
if (catCount > 0) return;

Se o banco estiver vazio, ele popula categorias e produtos padrão usando uma transaction:

const transaction = db.transaction(() => {
  categories.forEach((cat, catIdx) => {
    const result = insertCat.run(cat.name, cat.icon, catIdx + 1);
    const catId = result.lastInsertRowid;

    cat.items.forEach((item, itemIdx) => {
      insertItem.run(
        catId,
        item.name,
        item.ideal_qty || 0,
        item.unit || 'un',
        item.perishable ? 1 : 0,
        item.purchase_freq || 'Estoque',
        item.notes || '',
        itemIdx + 1
      );
    });
  });
});
const transaction = db.transaction(() => {
  categories.forEach((cat, catIdx) => {
    const result = insertCat.run(cat.name, cat.icon, catIdx + 1);
    const catId = result.lastInsertRowid;

    cat.items.forEach((item, itemIdx) => {
      insertItem.run(
        catId,
        item.name,
        item.ideal_qty || 0,
        item.unit || 'un',
        item.perishable ? 1 : 0,
        item.purchase_freq || 'Estoque',
        item.notes || '',
        itemIdx + 1
      );
    });
  });
});
const transaction = db.transaction(() => {
  categories.forEach((cat, catIdx) => {
    const result = insertCat.run(cat.name, cat.icon, catIdx + 1);
    const catId = result.lastInsertRowid;

    cat.items.forEach((item, itemIdx) => {
      insertItem.run(
        catId,
        item.name,
        item.ideal_qty || 0,
        item.unit || 'un',
        item.perishable ? 1 : 0,
        item.purchase_freq || 'Estoque',
        item.notes || '',
        itemIdx + 1
      );
    });
  });
});

Usar transaction aqui evita um estado parcial do banco caso alguma inserção falhe durante o seed.

Autenticação com JWT

O fluxo de autenticação é simples:

  1. O usuário registra ou faz login com email e senha

  2. O backend valida as credenciais

  3. A senha é comparada com bcrypt

  4. O backend gera um JWT

  5. O frontend salva o token e o usuário no localStorage

  6. Chamadas autenticadas enviam Authorization Bearer

No cliente, o header é montado centralmente:

function getHeaders(): HeadersInit {
  const headers: HeadersInit = {
    'Content-Type': 'application/json',
  };

  const token = localStorage.getItem('token');
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  return headers;
}
function getHeaders(): HeadersInit {
  const headers: HeadersInit = {
    'Content-Type': 'application/json',
  };

  const token = localStorage.getItem('token');
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  return headers;
}
function getHeaders(): HeadersInit {
  const headers: HeadersInit = {
    'Content-Type': 'application/json',
  };

  const token = localStorage.getItem('token');
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  return headers;
}

Isso evita repetir lógica de autenticação em cada chamada de API.

API REST

A API é organizada em recursos:

POST   /meu-mercado/api/auth/register
POST   /meu-mercado/api/auth/login
GET    /meu-mercado/api/categories
GET    /meu-mercado/api/items
GET    /meu-mercado/api/lists
POST   /meu-mercado/api/lists
GET    /meu-mercado/api/lists/:id
PUT    /meu-mercado/api/lists/:id
DELETE /meu-mercado/api/lists/:id
POST   /meu-mercado/api/lists/:id/clone
POST   /meu-mercado/api/lists/:id/items
DELETE /meu-mercado/api/lists/:id/items/:itemId
PATCH  /meu-mercado/api/lists/:id/items/:itemId
POST   /meu-mercado/api/auth/register
POST   /meu-mercado/api/auth/login
GET    /meu-mercado/api/categories
GET    /meu-mercado/api/items
GET    /meu-mercado/api/lists
POST   /meu-mercado/api/lists
GET    /meu-mercado/api/lists/:id
PUT    /meu-mercado/api/lists/:id
DELETE /meu-mercado/api/lists/:id
POST   /meu-mercado/api/lists/:id/clone
POST   /meu-mercado/api/lists/:id/items
DELETE /meu-mercado/api/lists/:id/items/:itemId
PATCH  /meu-mercado/api/lists/:id/items/:itemId
POST   /meu-mercado/api/auth/register
POST   /meu-mercado/api/auth/login
GET    /meu-mercado/api/categories
GET    /meu-mercado/api/items
GET    /meu-mercado/api/lists
POST   /meu-mercado/api/lists
GET    /meu-mercado/api/lists/:id
PUT    /meu-mercado/api/lists/:id
DELETE /meu-mercado/api/lists/:id
POST   /meu-mercado/api/lists/:id/clone
POST   /meu-mercado/api/lists/:id/items
DELETE /meu-mercado/api/lists/:id/items/:itemId
PATCH  /meu-mercado/api/lists/:id/items/:itemId

Um ponto interessante está na listagem de listas. A API retorna metadados agregados de progresso:

SELECT sl.*,
  COUNT(sli.id) as total_items,
  SUM(CASE WHEN sli.purchased = 1 THEN 1 ELSE 0 END) as purchased_items
FROM shopping_lists sl
LEFT JOIN shopping_list_items sli ON sl.id = sli.list_id
WHERE sl.user_id = ?
GROUP BY sl.id
ORDER BY sl.date DESC, sl.created_at DESC
SELECT sl.*,
  COUNT(sli.id) as total_items,
  SUM(CASE WHEN sli.purchased = 1 THEN 1 ELSE 0 END) as purchased_items
FROM shopping_lists sl
LEFT JOIN shopping_list_items sli ON sl.id = sli.list_id
WHERE sl.user_id = ?
GROUP BY sl.id
ORDER BY sl.date DESC, sl.created_at DESC
SELECT sl.*,
  COUNT(sli.id) as total_items,
  SUM(CASE WHEN sli.purchased = 1 THEN 1 ELSE 0 END) as purchased_items
FROM shopping_lists sl
LEFT JOIN shopping_list_items sli ON sl.id = sli.list_id
WHERE sl.user_id = ?
GROUP BY sl.id
ORDER BY sl.date DESC, sl.created_at DESC

Com isso, o frontend não precisa buscar todos os itens de todas as listas para renderizar o dashboard.

Frontend mobile-first

A interface foi pensada para uso no celular, que é o dispositivo mais provável durante uma compra.

O app trabalha com uma largura máxima de conteúdo e componentes grandes o suficiente para toque:

<div className="max-w-2xl mx-auto px-4 py-6 space-y-6">
<div className="max-w-2xl mx-auto px-4 py-6 space-y-6">
<div className="max-w-2xl mx-auto px-4 py-6 space-y-6">

O fluxo principal possui três telas:

  • Dashboard

  • Nova Lista

  • Lista de Compras

Em vez de usar um roteador externo, a navegação entre telas é controlada por estado local:

type View = 'dashboard' | 'new-list' | 'shopping-list';
const [view, setView] = useState<View>('dashboard');
type View = 'dashboard' | 'new-list' | 'shopping-list';
const [view, setView] = useState<View>('dashboard');
type View = 'dashboard' | 'new-list' | 'shopping-list';
const [view, setView] = useState<View>('dashboard');

Estado otimista na lista de compras

Na tela de compra, marcar um item como comprado atualiza a UI imediatamente:

setList(prev => prev ? {
  ...prev,
  purchased_items: (prev.purchased_items || 0) + (newPurchased ? 1 : -1),
  items: prev.items.map(i =>
    i.id === item.id
      ? { ...i, purchased: newPurchased ? 1 : 0, purchased_at: newPurchased ? new Date().toISOString() : null }
      : i
  ),
} : null);
setList(prev => prev ? {
  ...prev,
  purchased_items: (prev.purchased_items || 0) + (newPurchased ? 1 : -1),
  items: prev.items.map(i =>
    i.id === item.id
      ? { ...i, purchased: newPurchased ? 1 : 0, purchased_at: newPurchased ? new Date().toISOString() : null }
      : i
  ),
} : null);
setList(prev => prev ? {
  ...prev,
  purchased_items: (prev.purchased_items || 0) + (newPurchased ? 1 : -1),
  items: prev.items.map(i =>
    i.id === item.id
      ? { ...i, purchased: newPurchased ? 1 : 0, purchased_at: newPurchased ? new Date().toISOString() : null }
      : i
  ),
} : null);

Esse é um exemplo de optimistic UI: o usuário sente a resposta instantaneamente enquanto a API confirma a alteração.

Docker e deploy

O Dockerfile empacota backend e frontend no mesmo container:

FROM node:20-slim

WORKDIR /app

RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

COPY server/ ./server/
COPY dist/ ./dist/

RUN mkdir -p /app/data

ENV PORT=3001
ENV NODE_ENV=production
ENV DATA_DIR=/app/data
ENV JWT_SECRET=change-this-to-a-secure-random-string

EXPOSE 3001

VOLUME ["/app/data"]

CMD ["node", "server/index.js"]
FROM node:20-slim

WORKDIR /app

RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

COPY server/ ./server/
COPY dist/ ./dist/

RUN mkdir -p /app/data

ENV PORT=3001
ENV NODE_ENV=production
ENV DATA_DIR=/app/data
ENV JWT_SECRET=change-this-to-a-secure-random-string

EXPOSE 3001

VOLUME ["/app/data"]

CMD ["node", "server/index.js"]
FROM node:20-slim

WORKDIR /app

RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

COPY server/ ./server/
COPY dist/ ./dist/

RUN mkdir -p /app/data

ENV PORT=3001
ENV NODE_ENV=production
ENV DATA_DIR=/app/data
ENV JWT_SECRET=change-this-to-a-secure-random-string

EXPOSE 3001

VOLUME ["/app/data"]

CMD ["node", "server/index.js"]

O volume em /app/data garante persistência do SQLite mesmo recriando containers.

Evolução futura com Flutter

Uma evolução natural do projeto seria transformar o sistema em um app mobile nativo usando Flutter.

O objetivo seria manter a filosofia local-first:

  • listas funcionando offline

  • dados persistidos no dispositivo

  • sincronização opcional com backend

Arquitetura prevista:

Flutter App
  |
  | leitura/escrita local
  v
SQLite no dispositivo
  |
  | sincronização opcional
  v
API Node/Express
  |
  v
SQLite/Postgres no servidor
Flutter App
  |
  | leitura/escrita local
  v
SQLite no dispositivo
  |
  | sincronização opcional
  v
API Node/Express
  |
  v
SQLite/Postgres no servidor
Flutter App
  |
  | leitura/escrita local
  v
SQLite no dispositivo
  |
  | sincronização opcional
  v
API Node/Express
  |
  v
SQLite/Postgres no servidor

Isso permitiria:

  • uso offline no mercado

  • sincronização entre dispositivos

  • backup opcional

  • futura publicação em iOS e Android

Trade-offs da arquitetura

Toda arquitetura possui escolhas.

Neste projeto, alguns trade-offs são claros:

JWT em localStorage

Simples e funcional, mas menos seguro que cookies HttpOnly.

Navegação por estado local

Reduz complexidade, mas não gera URLs compartilháveis.

SQLite embarcado

Excelente para baixo custo e deploy simples, mas limitado em cenários muito concorrentes.

Backend servindo frontend

Simplifica deploy, mas acopla frontend e API.

Conclusão

O Meu Mercado é um exemplo de aplicação full-stack pragmática:

  • resolve um problema real

  • usa uma stack enxuta

  • evita infraestrutura desnecessária

  • mas ainda segue boas práticas de engenharia

A maior virtude do projeto está na coerência entre escopo e arquitetura.

Ele não tenta parecer maior do que é, mas também não ignora fundamentos importantes:

  • autenticação

  • persistência

  • transações

  • organização de dados

  • deploy reproduzível

  • experiência mobile-first

Para produtos internos, operações pequenas ou projetos pessoais, a combinação de React + Express + SQLite + Docker entrega muito valor com baixa complexidade operacional.

Conteúdo original pesquisado e redigido pelo autor. Ferramentas de IA podem ter sido utilizadas para auxiliar na edição e no aprimoramento.

Conteúdo original pesquisado e redigido pelo autor. Ferramentas de IA podem ter sido utilizadas para auxiliar na edição e no aprimoramento.

Posts relacionados:

Posts relacionados:

Compartilhe!

Go back

Deixe a IA fazer o trabalho para Você Crescer Mais Rápido

Agende uma conversa hoje e comece a automatizar.

Deixe a IA fazer o trabalho para Você Crescer Mais Rápido

Agende uma conversa hoje e comece a automatizar.

© 2010 - 2026 Copyright

All Rights Reserved - Develop by Ad Rock Digital Mkt

Tecnologias utilizadas

© 2010 - 2026 Copyright

All Rights Reserved - Develop by
Ad Rock Digital Mkt

Tecnologias utilizadas