30 de abril de 2022 • 6 min de leitura
Miniminalismo com Node
A web e Node / Js tem sido sinônimos e hoje vou escrever um pouco sobre o que tenho estudado sobre isso e algumas referências que me ajudam a cada dia a ser um dev melhor
Referências
Antes de começar esse resumo do Tao of Node do Alexander Kondov, caso queria a fonte original de muito do que eu falo, seguem elas aqui:
Com essas referências eu acredito que teremos alguma base para o que falaremos hoje nesse blogpost. O projeto que eu usei como modelo é esse daqui
No começo
Independente que tipo de projeto você irá fazer em node, vamos falar um pouco sobre filosofia, em node temos uma ideia que o pequeno é bonito, apenas o necessário. Minimalista. O que isso gera ? temos pequenos pacotes ou modulos que fazem algo muito bem feito e provavelmente é mantido pela comunidade. Sim o NPM ou Yarn é algo que faz parte da filosofia do Node e seus pacotes levam isso consigo. Express é o maior exemplo e por sua vez é quase que sinônimo de node, TypeScript por ser literalmente JavaScript com tempero também é muito bem aceito... React e muitos outros são apenas JS com temperinho mas um tempero muito bem feito.
Setup
Óbvio que como criamos um projeto em 2022, usaremos TypeScript que é uma solução para lidarmos com o aumento de nossa codebase, também usaremos fastify, mais por opção propria por gostar da filosofia deles e de ter algumas coisas out of the box mas express ainda é o grande framework/lib do node.
Também gosto de ressaltar que por preferência uso o MongoDB, mas isso é mais detalhe de como serão guardados do que sobre como seu código é estruturado. Cada modelo ou domínio da aplicação deverá ter seu próprio diretório e seguir lá com suas complexidades, deixando assim o mais simples e fácil visualização. No exemplo temos apenas dois domínios em nossa aplicação de petshop, os Pets e os Customers:
Controllers
Quando falamos de controllers elas são a nossa fachada, onde o front bate, pede ou simplismente mexe, é a nossa API. Quando se pensa em API ela tem que ser simples mas ao mesmo tempo eficiente em seu trabalho, fazer o que se precisa. Nesse crud minha fachada de Customer ficou dessa forma:
export async function CustomerController(fastify: FastifyInstance) {
const customerService = CustomerService(fastify);
const petService = PetService(fastify);
fastify.get<{ Reply: Array<CustomerSchema> }>
('/customers',
async (
request: FastifyRequest, reply: FastifyReply
) => {
const result = await customerService.getAllCustomers()
if (result.length === 0) {
reply.status(404);
throw new Error('No documents found')
}
reply.status(200).send(result);
});
fastify.get<{ Params: { customerID: string }, Reply: CustomerSchema }>
('/customers/:customerID',
async (
request: FastifyRequest<{ Params: { customerID: string } }>,
reply: FastifyReply
) => {
const {customerID} = request.params;
const result = await customerService.getCustomerById(customerID);
if (!result) {
reply.status(404).send(customerID);
throw new Error('Invalid value');
}
reply.status(200).send(result);
});
fastify.get<{ Params: { customerID: string }, Reply: CustomerSchema }>
('/customers/:customerID/pets',
async (
request: FastifyRequest<{ Params: { customerID: string } }>,
reply: FastifyReply
) => {
const {customerID} = request.params;
const customer = await customerService.getCustomerById(customerID);
if (!customer) {
reply.status(404).send('Invalid user id');
throw new Error('Invalid user id');
}
if (customer.pets === undefined || customer.pets?.length === 0) {
reply.status(400).send('No pets were added');
throw new Error('No pets were added');
}
const res = await petService.getPetsByIds(customer.pets).toArray();
if (res === null) {
reply.status(500).send('DB broke');
throw new Error('Something is wrong');
}
reply.status(200).send(res);
});
fastify.put<{ Body: CustomerSchema, Reply: CustomerSchema, Params: { customerID: string } }>
('/customers/:customerID',
async (
request: FastifyRequest<{ Body: CustomerSchema, Params: { customerID: string } }>,
reply: FastifyReply
) => {
const {customerID} = request.params;
const customer = request.body;
const result = await customerService.updateCustomer(customerID, customer);
if (result.ok === 0) {
reply.status(400).send(customer);
}
reply.status(200).send(customer);
});
fastify.post<{ Body: CustomerSchema, Reply: CustomerSchema }>
('/customers',
async (
request: FastifyRequest<{ Body: CustomerSchema, Reply: CustomerSchema }>,
reply: FastifyReply
) => {
const customer = request.body;
const createdCustomer = await customerService.createCustomer(customer);
reply.status(200).send(createdCustomer);
});
}
Vendo essa controller podemos inferir algumas coisas, diferente mas muito similar á um projeto em uma linguagem orientada a objetos, temos uma injeção de dependencia no começo dela, quando chamamos os dois services, e toda controller acontece num contexto de uma Função.
Única responsabilidade da controller é controlar o fluxo, chamar as funções e dai retornar erro ou os dados, sem acessar regra de négocio / Banco de dados.
Vamos seguir a ordem das partes lógicas do código, por próximo, iremos falar da service e o que ela deve ter de responsabilidade.
Services
Quando se fala em services, pensa em duas partes, quem chama o banco de dados ou context e lidar com regras de negócio. No caso de um projeto simples como esse a service chama o DB e lida as gravações apenas.
export default function PetService(
fastify: FastifyInstance<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>
) {
const db = PetContext(fastify);
const getAllPets = () => {
return db.find().toArray();
}
const getPetById = (id: string) => {
return db.findOne(new ObjectId(id))
}
const getPetsByIds = (ids: Array<string>) => {
const i = ids.map($ => new ObjectId($));
return db.find( {_id: {$in: i}} );
}
const updatePet = (id: string, pet: PetSchema) => {
return db.findOneAndReplace({_id: new ObjectId(id)}, pet);
}
const createPet = (pet: PetSchema) => {
return db.insertOne(pet);
}
const deletePet = (id: string) => {
return db.deleteOne({_id: new ObjectId(id)});
}
return {getAllPets, getPetById, updatePet, createPet, getPetsByIds, deletePet}
}
Como pode ser visto no código acima, esse service é um conjunto de funções que por sua vez recebem no parametro o código que será guardado no banco de dados.
Context
O context ou banco de dados é o arquivo onde lidaremos com isso. O arquivo pet-context
é nada mais que um arquivo onde nosso foco é conectar com nossa fonte de dados e dar um tipo ou schema a ela.
export default function PetContext(fastify: FastifyInstance<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>) {
if (fastify.mongo.db !== undefined) {
return fastify.mongo.db.collection<PetSchema>('Pets');
}
throw new Error('No DB collection found')
}
Simples não ? é por ser mongo e muito da complexidade estar no schema, mas as migrations e outras tarefas relacionadas a dados deveriam estar nesse context,ou seja em um diretorio onde exporta apenas o DB e as suas funcionalidades ficam escondidas, nesse caso é só a exportação da collection.
Schema
Schema é a representação do teu dado, pode ser um type + Objeto, é onde a base do teu dominio irá residir, se você tem um schema no banco e alguns outros detalhes, tudo isso ficará dentro desse diretório. O importante é ser claro para quem toca no projeto os dominios e a possibilidade de extensão através de diretorios e arquivos.
sem mais delongas o Schema de pet:
export const Pet = Type.Object({
name: Type.String(),
type: Type.Optional(Type.String()),
ownerID: Type.Optional(Type.String()),
});
export type PetSchema = Static<typeof Pet>;
Olha só, temos um Pet que é o schema do banco e o type dele que é usado pelo TypeScript. É essa simplicidade que tem que ser buscada em projetos de node, simples, fazendo apenas uma coisa, mas fazendo essa única coisa muito bem.
Resumo
Em resumo, devemos olhar com simplicidade e minimalismo com nossos backends, não buscar criar mais código que o necessário, sempre tentar deixar a entropia do código próxima de zero, para que a manutenção seja possivél.
Recomendo a leitura dos links dispostos no começo, uma vez que a fonte original por mais que seja um pouco mais difícil é o conteudo em natura e muitas vezes mais eficiente para o aprendizado.
Contato
Se quiser discutir sobre qualquer assunto ou viu algum erro, não hesite em me marcar ou chamar no Twitter: @que_cara_legal
Estou sempre tentando trazer o que tenho estudado, as vezes traduzindo algums tópicos divertidos que me chamam atenção.