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

Node

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:

Domains

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.