Atualizado em 30/12/23!
Quando estamos trabalhando em diversos projetos com Node.js, seja qual for a natureza deles, é muito comum haver a necessidade de compartilhamento de módulos para evitar duplicação de código e facilitar a manutenção futura. Um desses módulos muito comuns de precisarem de compartilhamento são os de acesso a dados, seja usando ORMs ou não. No entanto, dependendo das tecnologias e arquitetura escolhidas isso pode se tornar um desafio bem grande, como por exemplo no caso de compartilhar o schema ou o client do ORM Prisma.
O Prisma usa um CLI para gerar seu client a partir de um arquivo schema.prisma. Esse client é gerado baseando-se nas configurações definidas no projeto e uma vez gerado pode ser usado para manipulação do banco de dados pela aplicação. No entanto, por não ser o schema.prisma um módulo JS tradicional, não conseguimos compartilhar ele entre os diferentes projetos usando métodos convencionais que falei neste outro post. Mas calma, que tem solução.
Não vou falar aqui de publicação no NPM e nem no GitHub, que seriam duas alternativas bem viáveis, muito profissionais (principalmente NPM por permitir versionamento de módulos) e também e as mais fáceis de fazer, mas que no entanto exigem que você tenha os seus módulos compartilhados como públicos (free) ou como privados, mas pagando para o NPM. Também não vou entrar em alternativas mais complexas de fazer gestão de monorepo usando Lerna ou Rush. Vou propor uma abordagem mais light e que pode resolver o seu problema, como resolveu o meu.
Ah, entendo aqui que você está usando uma abordagem monorepo para o versionamento do seu projeto, ou seja, guarda todos eles em um mesmo repositório.
#1 – Estruturando a Commons
Para o exemplo prático, vou criar um monorepo chamado prisma-monorepo com dois projetos dentro:
- _commons_: projeto comum aos demais do monorepo, onde teremos os módulos compartilhados;
- application: projeto onde está uma aplicação que precisa usar os módulos da commons;
Vamos começar preparando a commons, a começar pela inicialização:
1 2 3 |
npm init -y |
Depois instale as dependências que vamos precisar:
1 2 3 |
npm i -D prisma typescript |
Inicialize o TypeScript:
1 2 3 |
npx tsc --init |
E inicialize o Prisma:
1 2 3 |
npx prisma init |
Será gerado um .env, mas você pode ignorá-lo pois cada projeto vai ter o seu. Vá no seu prisma/schema.prisma e ajuste de acordo com seu banco de dados normalmente. Apenas atenção a esta configuração adicional de output, que diz onde deve ser salvo o Prisma Client que vamos gerar:
1 2 3 4 5 6 |
generator client { provider = "prisma-client-js" output = "../data" } |
Após a configuração do schema.prisma, rode o comando abaixo para gerar o Prisma Client:
1 2 3 |
npx prisma generate |
Isso vai fazer com que a pasta data no projeto commons fique populada e pronta para ser usada como Prisma Client.
#2 – Aplicação de Exemplo
Agora vamos para a pasta application, onde teremos uma aplicação de exemplo que vai usar o Prisma Client da commons.
Vamos começar inicializando este segundo projeto:
1 2 3 |
npm init -y |
E adicionando as dependências que vamos precisar:
1 2 3 4 |
npm i -D typescript ts-node npm i dotenv |
Agora crie um arquivo .env na raiz desse projeto de aplicação e inclua nele a seguinte variável, preenchendo-a com a sua connection string correta (no meu exemplo estou usando MongoDB).
1 2 3 |
DATABASE_URL=mongodb://127.0.0.1:27017/workshoptdc |
Na sequência, inicialize o TypeScript nesse projeto:
1 2 3 |
npx tsc --init |
E ajuste o package.json para inicializar o projeto com TS Node:
1 2 3 4 5 |
"scripts": { "start": "ts-node index" }, |
E por fim, crie o arquivo index.ts que neste exemplo vai usar o Prisma Client da commons:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import dotenv from "dotenv"; dotenv.config(); import { PrismaClient } from "../_commons_/data"; async function getCustomers() { const prisma = new PrismaClient(); return prisma.customers.findMany(); } async function start() { const result = await getCustomers(); console.log(result); } start(); |
Repare que carreguei o .env antes do Prisma Client, pois internamente ele depende da variável DATABASE_URL. Repare também que importei o PrismaClient a partir da pasta data da commons. Depois, não tem nenhum código específico para este exemplo, sendo tudo de acordo com o seu schema e uso normal do Prisma.
O que você tem de tomar cuidado com esta abordagem é apenas com o fato de que toda vez que mudar seu schema.prisma, precisará mandar gerá-lo de novo e retestar todos projetos que usam a pasta data da commons. Nada diferente do que você já teria de cuidar com o Prisma em single project, mas que não custa eu reforçar.
#3 – Mapeando o Caminho
Dependendo da estrutura de pastas que você tiver, como no meu caso onde costumo ter uma pasta “data” dentro da _commons_ com os arquivos do Prisma Client dentro, pode ser que o caminho fique um tanto comprido nos seus imports do projeto. Para resolver isso podemos remapear o caminho até seu projeto commons, sendo o primeiro passo instalar um novo pacote.
1 2 3 |
npm i tsconfig-paths |
Depois, vá no seu tsconfig.json e ajuste quatro configurações:
1 2 3 4 5 6 7 8 |
"outDir": "./dist/", "baseUrl": ".", "paths": { "commons/*": ["../_commons_/*"] }, "rootDirs": ["./", "../_commons_"], |
A primeira configuração é apenas a definição da pasta onde ficarão os .JS compilados a partir dos .TS, provavelmente você já tem esta configuração na sua aplicação TypeScript.
A segunda configuração diz a URL base para resolução de caminhos de arquivos e é pré-requisito para a segunda configuração, só por isso defini ela (usei . que quer dizer “pasta atual”).
A terceira configuração define aliases (apelidos) para os caminhos relativos que queremos mapear os nossos módulos compartilhados. Aqui estou dizendo que todos módulos que começarem com commons/ na verdade devem ser resolvidos para ../_commons_/ (ou seja, estão um nível acima em relação a URL base).
E por último, para que não dê erro em tempo de desenvolvimento, a configuração rootDirs diz pro TypeScript onde estão todos os arquivos .TS necessários para a correta interpretação (em tempo de dev) e compilação do projeto como um todo. Assim, além de olhar na pasta raiz do projeto, o TS vai olhar na pasta _commons_ também.
#4 – Cuidados no NestJS
Se você estiver trabalhando com NestJS tem de tomar alguns cuidados adicionais com esta abordagem proposta no artigo. Isso porque ele vai se perder pois na pasta do build (que eu costumo chamar de pasta “dist”) vão constar os projetos do seu backend e da commons, em pastas separadas, além do que na compilação não serão copiados os arquivos do seu Prisma Client (pasta “data”).
Sendo assim, você precisa ajustar o seu script de start para que ele procure o arquivo main na pasta certa e também ajustar o script de build para que ele copie também os arquivos de commons/data, a fim de que funcione o Prisma Client na versão compilada.
Primeiro, vamos ajustar o nest-cli.json para que ele procure o arquivo main no lugar certo (entryFile) e para que não exclua a pasta dist a cada compilação.
1 2 3 4 5 6 7 8 9 10 11 |
{ "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "entryFile": "backend/src/main", "compilerOptions": { "deleteOutDir": false } } |
Depois, vamos no package.json do projeto Nest, ajustar a seção de scripts (exibo abaixo apenas scripts que sofreram mudanças).
1 2 3 4 5 6 7 8 9 10 |
"scripts": { "copy-files": "yes | cp -rf ../_commons_/data/ ./dist/_commons_/data", "build": "nest build && npm run copy-files", "start": "npm run build && nest start", "start:dev": "npm run build && nest start --watch", "start:debug": "npm run build && nest start --debug --watch", "start:prod": "node dist/backend/src/main", }, |
O ponto mais importante aqui é o novo script copy-files, que faz uma cópia de todo o conteúdo da pasta “data” da commons para a dist (as flags -rf servem para agir recursivamente e sobrescrevendo arquivos existentes, enquanto que o ‘yes’ confirma essa ação), garantindo que o Prisma Client esteja onde ele deve estar. Agora temos de chamar esse script copy-files com “npm run copy-files” sempre que quizermos que os arquivos sejam copiados. Opcionalmente, você pode incluir o “npx prisma generate” aqui também, para garantir que estará com a última versão dos schemas e da API.
Depois, usamos esse script de cópia de arquivos nos demais scripts em que ele for necessário, garantindo que ele seja chamado no momento certo.
Um último ponto que vale ser citado é o ajuste que fiz no script start:prod para que o arquivo main seja encontrado na nova estrutura de pastas que vai ter dentro da dist (meu projeto de backend está em uma pasta chamada “backend”).
Espero ter ajudado!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.