Cómo consumir APIs en React sin ensuciar tus componentes
Aprende cómo transformar datos entre React y tu backend usando adapters y mappers para crear componentes más fáciles de mantener y escalar.
Gabriel Jiménez | Hace 6 días
Muchas veces una aplicación comienza con unos cuantos componentes y carpetas simples. Pero conforme el proyecto crece, comienzan a aparecer problemas: archivos difíciles de encontrar, lógica duplicada, componentes mezclando demasiadas responsabilidades y una estructura que nadie entiende realmente. El problema no siempre es React, es la forma en la que organizamos el proyecto.
Una mala organización de carpetas y archivos puede hacer que desarrollar nuevas funcionalidades sea más lento, que hacer refactorizaciones dé miedo y que escribir pruebas sea complicado.
En este artículo aprenderás cómo organizar tus carpetas y archivos en React de una forma más clara y escalable. Veremos errores comunes, estrategias para estructurar proyectos pequeños y grandes, y cómo una buena organización puede ayudarte a escribir componentes más fáciles de mantener, reutilizar y probar.
Organizar nuestras carpetas no es una tarea al azar. Es una parte fundamental del desarrollo de software y algo que, como desarrolladores, deberíamos tomar en serio desde las primeras etapas del proyecto.
Cuando una aplicación es pequeña, la organización de carpetas parece no importar demasiado. Todo es fácil de encontrar y mover archivos no representa un problema. Pero conforme el proyecto crece, comienzan a aparecer señales de alerta: archivos difíciles de ubicar, lógica mezclada en lugares incorrectos, componentes gigantes y una estructura que cada vez cuesta más entender.
Con el tiempo esto se convierte en “código espagueti” — solo dios y la IA saben como funciona. Este tipo de código vuelve lento, frustrante y riesgoso seguir agregando o modificando nuestros proyectos.
Ahora bien, organizar carpetas desde el inicio no significa diseñar la arquitectura perfecta desde el día uno. De hecho, intentar hacerlo suele llevar a sobreingeniería. Lo normal es comenzar con una estructura simple y permitir que el mismo proyecto vaya comunicando sus necesidades conforme evoluciona.
Una buena organización no busca verse “bonita”. Busca facilitar el desarrollo, el mantenimiento, los refactors y las pruebas a largo plazo.
Estos son algunos de los problemas más comunes al momento de organizar nuestras carpetas y archivos:
Muchas veces nunca escuchamos sobre las ventajas de tener una estructura clara, por lo que terminamos organizando archivos únicamente “como dios nos dio a entender”.
Por ejemplo, guardar todos los componentes de formularios dentro de una sola carpeta. Aunque al inicio parece buena idea, conforme el proyecto crece comenzamos a mezclar funcionalidades completamente diferentes en un mismo lugar.
Por ejemplo, imponer reglas como que cada carpeta debe tener obligatoriamente una subcarpeta llamada components. Este tipo de estructuras rígidas suelen agregar complejidad innecesaria y dificultan adaptar el proyecto a nuevas necesidades.
Muchas veces creemos que nuestro proyecto crecerá rápidamente y comenzamos creando arquitecturas complejas desde el primer día. Sin embargo, la realidad es que muchos proyectos se mantienen relativamente simples o incluso terminan abandonándose antes de necesitar esa complejidad.
Construimos software para resolver problemas reales del día a día. Desde gestionar las citas de una clínica veterinaria hasta registrar inversiones o controlar ventas dentro de un negocio.
Y precisamente esos mismos problemas de negocio suelen darnos la mejor pista sobre cómo deberíamos organizar nuestras carpetas y archivos.
Por ejemplo, imaginemos que estamos desarrollando el flujo para registrar citas en línea dentro de una clínica veterinaria. Tenemos un formulario con campos como paciente, nombre del dueño, motivo de la visita y fecha de la cita.
En lugar de colocar el formulario dentro de una carpeta genérica como forms, podríamos encapsular todo este flujo dentro de una carpeta llamada VisitasEnLinea. Dentro de ella viviría todo lo relacionado con esa funcionalidad: componentes, hooks, validaciones, servicios, pruebas y cualquier otra pieza necesaria para resolver ese problema.
De esta manera, comenzamos a organizar nuestro proyecto alrededor de funcionalidades y no únicamente por tipos de archivos. Esto tiene una ventaja enorme: cualquier persona que trabaje sobre el proyecto podrá entender mucho más rápido qué problema de negocio está resolviendo cada carpeta. En otras palabras, la estructura del proyecto comienza a comunicar intención y contexto, en lugar de solo almacenar archivos.
Para este ejemplo práctico, imaginemos el siguiente caso:
Una clínica veterinaria necesita automatizar el flujo de seguimiento de expedientes. Para resolver esta tarea, necesitamos una página para dar de alta un expediente, editarlo, listar los expedientes existentes y consultar el detalle de un paciente.
Una posible estructura de carpetas podría ser la siguiente:
LiusVET
src
Expedientes
ExpedientesForm
ExpedientesEdit
ExpedientesNew
ExpedientesDetails
Donde:
La ventaja de esta estructura es que cada carpeta representa una funcionalidad concreta dentro del módulo de expedientes. No estamos agrupando archivos únicamente porque “son componentes” o “son formularios”, sino porque todos colaboran para resolver una parte específica del flujo.
Si mañana necesitamos agregar una nueva funcionalidad dentro del expediente, podríamos seguir la misma convención usando nombres como ExpedientesHistorial, ExpedientesVacunas o ExpedientesArchivos. Ahora, si esa nueva funcionalidad también tiene un flujo tipo CRUD, podríamos repetir la misma idea: una carpeta para el listado, otra para el formulario, otra para el alta, otra para la edición y otra para el detalle.
Lo importante no es copiar esta estructura al pie de la letra, sino entender la intención: organizar el proyecto alrededor de funcionalidades claras que ayuden a cualquier persona a ubicarse rápidamente dentro del código.
Al igual que la organización de carpetas, tener archivos con una intención clara y siguiendo un patrón definido hace mucho más sencillo escalar nuestras aplicaciones con el tiempo.
Cuando los archivos no tienen una responsabilidad clara, es común terminar con componentes gigantes, lógica mezclada y nombres difíciles de entender. En cambio, una estructura consistente permite identificar rápidamente qué hace cada archivo y dónde debería vivir una nueva funcionalidad.
A continuación, veamos algunos ejemplos prácticos de cómo podemos estructurar nuestros archivos de una manera más clara y mantenible.
Un archivo entry por carpeta
Cada carpeta debería tener un archivo principal que funcione como punto de entrada. Este archivo representa la funcionalidad completa de esa carpeta.
src
Expedientes
Expedientes.jsx
ExpedientesTable.jsx
getExpedientes.js
// Expedientes/Expedientes.jsx
import ExpedientesTable from "./ExpedientesTable";
import { getExpedientes } from "./getExpedientes";
export default function Expedientes() {
return <ExpedientesTable />;
}
El archivo entry debe tener el mismo nombre de la carpeta
Esto ayuda a identificar rápidamente cuál es el archivo principal.
src
ExpedientesNew
ExpedientesNew.jsx
ExpedientesForm.jsx
createExpediente.js
// ExpedientesNew/ExpedientesNew.jsx
import ExpedientesForm from "./ExpedientesForm";
import { createExpediente } from "./createExpediente";
export default function ExpedientesNew() {
return <ExpedientesForm onSubmit={createExpediente} />;
}
Todos los archivos deben vivir en el mismo nivel
Evita crear subcarpetas innecesarias dentro de cada funcionalidad. Si la carpeta todavía es pequeña, mantener todo al mismo nivel facilita encontrar los archivos.
src
ExpedientesEdit
ExpedientesEdit.jsx
ExpedientesForm.jsx
getExpediente.js
updateExpediente.js
normalizeExpedientePayload.js
Cualquier archivo que no sea entry debe expresar claramente su intención
El nombre del archivo debería decir qué hace, sin tener que abrirlo.
ExpedientesEdit
ExpedientesEdit.jsx
getExpediente.js
updateExpediente.js
normalizeExpedientePayload.js
// ExpedientesEdit/normalizeExpedientePayload.js
export function normalizeExpedientePayload(data) {
return {
patient_name: data.patientName,
owner_name: data.ownerName,
reason: data.reason,
};
}
Los hooks deben seguir la convención useIntención
Los hooks personalizados deben comenzar con use y expresar claramente qué lógica encapsulan.
// hooks/useExpedienteForm.js
import { useState } from "react";
export function useExpedienteForm(initialValues) {
const [data, setData] = useState(initialValues);
function updateField(field, value) {
setData((currentData) => ({
...currentData,
[field]: value,
}));
}
return {
data,
updateField,
};
}
Los hooks deben vivir dentro de una carpeta hooks en la raíz del proyecto
Cuando un hook se comparte entre diferentes funcionalidades, puede vivir en una carpeta global.
src
hooks
useExpedienteForm.js
useDebounce.js
usePagination.js
// ExpedientesNew/ExpedientesNew.jsx
import { useExpedienteForm } from "../hooks/useExpedienteForm";
export default function ExpedientesNew() {
const { data, updateField } = useExpedienteForm({
patientName: "",
ownerName: "",
reason: "",
});
return null;
}
Los componentes reutilizables deben vivir en components
Si un componente se utiliza en diferentes partes del proyecto, no debería pertenecer a una funcionalidad específica.
src
components
Button.jsx
TextInput.jsx
Card.jsx
// components/TextInput.jsx
export default function TextInput({ label, value, onChange }) {
return (
<label>
{label}
<input value={value} onChange={onChange} />
</label>
);
}
Y luego puede utilizarse desde cualquier funcionalidad:
// ExpedientesForm.jsx
import TextInput from "../components/TextInput";
export default function ExpedientesForm() {
return (
<TextInput
label="Nombre del paciente"
value=""
onChange={() => {}}
/>
);
}
Los archivos de prueba deben vivir al mismo nivel del archivo que están probando
El código y sus pruebas viven cerca uno del otro, haciendo mucho más sencillo encontrar, mantener y actualizar las pruebas conforme el proyecto evoluciona.
ExpedientesEdit ExpedientesEdit.jsx ExpedientesEdit.test.jsx ExpedientesForm.jsx ExpedientesForm.test.jsx getExpediente.js getExpediente.test.js normalizeExpedientePayload.js normalizeExpedientePayload.test.js
Cómo una buena estructura facilita el testing en React
Tener una estructura fácil de leer y modificar permite encapsular responsabilidades de una manera mucho más clara. Y cuando las responsabilidades están bien separadas, escribir pruebas automatizadas se vuelve mucho más sencillo.
En otras palabras, una buena organización de carpetas y archivos no solo mejora la mantenibilidad del proyecto, también facilita probar cada pieza de forma aislada.
Veamos un ejemplo práctico de cómo una buena estructura puede facilitar el testing en React.
Supongamos que tenemos la siguiente estructura:
ExpedientesEdit ExpedientesEdit.jsx ExpedientesForm.jsx getExpediente.js updateExpediente.js normalizeExpedientePayload.js
Gracias a esta separación, podemos probar cada responsabilidad de forma independiente.
Por ejemplo, podemos probar la normalización del payload sin necesidad de renderizar componentes de React:
// normalizeExpedientePayload.test.js
import { normalizeExpedientePayload } from "./normalizeExpedientePayload";
describe("normalizeExpedientePayload", () => {
it("normaliza correctamente el payload", () => {
const payload = normalizeExpedientePayload({
patientName: "Max",
ownerName: "Gabriel",
reason: "Vacunación",
});
expect(payload).toEqual({
patient_name: "Max",
owner_name: "Gabriel",
reason: "Vacunación",
});
});
});
También podemos probar el componente del formulario de manera aislada:
// ExpedientesForm.test.jsx
import { render, screen } from "@testing-library/react";
import ExpedientesForm from "./ExpedientesForm";
describe("ExpedientesForm", () => {
it("muestra el campo nombre del paciente", () => {
render(<ExpedientesForm />);
expect(
screen.getByLabelText("Nombre del paciente")
).toBeInTheDocument();
});
});
Y finalmente, podemos probar el flujo principal sin preocuparnos por la lógica interna de cada archivo:
// ExpedientesEdit.test.jsx
import { render, screen } from "@testing-library/react";
import ExpedientesEdit from "./ExpedientesEdit";
describe("ExpedientesEdit", () => {
it("renderiza el formulario de edición", () => {
render(<ExpedientesEdit />);
expect(
screen.getByText("Editar expediente")
).toBeInTheDocument();
});
});
La ventaja de esta estructura es que cada archivo tiene una responsabilidad clara. Esto evita tener componentes gigantes con demasiada lógica mezclada y permite escribir pruebas más pequeñas, entendibles y fáciles de mantener.
Al final, una buena estructura no solo ayuda a encontrar archivos más rápido. También ayuda a crear software más fácil de probar, refactorizar y escalar.
Organizar correctamente nuestras carpetas y archivos permite crear aplicaciones más fáciles de mantener, escalar y probar. No necesitas una estructura perfecta desde el inicio. Lo importante es comenzar simple y permitir que el mismo proyecto vaya comunicando sus necesidades conforme crece.
Muchas veces, cuando una funcionalidad comienza a generar demasiado ruido, es una señal de que llegó el momento de encapsularla mejor mediante una estructura más clara.
Si quieres aprender cómo el testing puede ayudarte a crear aplicaciones más fáciles de escalar, mantener y refactorizar sin miedo a romper funcionalidades, te recomiendo visitar mi hub de Testing en React.
Aprende cómo transformar datos entre React y tu backend usando adapters y mappers para crear componentes más fáciles de mantener y escalar.
Aprende cuándo crear hooks personalizados en React para reutilizar lógica, simplificar componentes y mejorar el testing.
Aprende por qué algunos componentes en React son difíciles de probar y cómo organizarlos para crear código más escalable y mantenible.
Aprende a detectar componentes difíciles de mantener en React antes de que se conviertan en deuda técnica y errores costosos.