Miniminalismo com Node

grubba

Gabriel Grubba

Posted on July 21, 2022

Miniminalismo com Node

Tao symbol

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);
        });
}
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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')
}
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
grubba
Gabriel Grubba

Posted on July 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related