Compartir en Twitter
Go to Homepage

CONSTRUYE UNA APLICACIÓN TODO FULL-STACK CON REACT, TYPESCRIPT, NODE.JS Y MONGODB

January 11, 2026

Introducción al desarrollo de una aplicación de tareas completa

El desarrollo de aplicaciones web modernas requiere el uso de tecnologías robustas que permitan crear soluciones escalables y mantenibles. En este tutorial, exploraremos la construcción de una aplicación de gestión de tareas utilizando React con TypeScript en el frontend, y Node.js con Express y MongoDB en el backend. Esta combinación, conocida comúnmente como stack MERN con TypeScript, ofrece una experiencia unificada al emplear JavaScript en ambos lados del desarrollo.

La aplicación permitirá realizar operaciones CRUD completas: crear, leer, actualizar y eliminar tareas. Cada tarea contendrá un nombre, una descripción y un estado que indica si está completada o no. La comunicación entre el cliente y el servidor se realizará mediante una API REST, lo que facilita la separación de responsabilidades y la reutilización de componentes.

Este enfoque es particularmente útil en entornos profesionales actuales, donde la tipificación estática proporcionada por TypeScript ayuda a reducir errores en tiempo de compilación y mejora la legibilidad del código. Además, MongoDB como base de datos NoSQL permite una modelación flexible de los datos, ideal para aplicaciones que evolucionan rápidamente.

A lo largo del tutorial, configuraremos el entorno de desarrollo, definiremos los modelos de datos, implementaremos los controladores y rutas en el backend, y construiremos una interfaz reactiva en el frontend. Se incluirán ejemplos prácticos de código para ilustrar cada concepto, asegurando que el lector pueda replicar el proyecto de manera independiente.

Configuración inicial del proyecto backend

El backend se construye con Node.js, Express y TypeScript. Comenzamos inicializando el proyecto.

Ejecuta los siguientes comandos en la terminal:

mkdir todo-backend
cd todo-backend
yarn init -y
yarn add express cors mongoose dotenv
yarn add -D typescript @types/express @types/node @types/cors nodemon concurrently ts-node
npx tsc --init

Esto crea la estructura básica y agrega las dependencias necesarias. Actualiza el archivo tsconfig.json con opciones modernas recomendadas para 2026:

{
    "compilerOptions": {
        "target": "ES2022",
        "module": "ESNext",
        "moduleResolution": "node",
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
    },
    "include": ["src"]
}

Agrega scripts útiles en package.json:

{
    "scripts": {
        "build": "tsc",
        "start": "node dist/app.js",
        "dev": "concurrently \"tsc -w\" \"nodemon dist/app.js\""
    }
}

Crea un archivo .env para variables de entorno:

MONGO_USER=tu_usuario
MONGO_PASSWORD=tu_contraseña
MONGO_DB=tu_base_de_datos
PORT=4000

La estructura de directorios recomendada es la siguiente:

src/
  app.ts
  controllers/
    todos/
      index.ts
  models/
    todo.ts
  routes/
    index.ts
  types/
    todo.ts

Esta organización facilita el mantenimiento a medida que el proyecto crece.

Definición de tipos y modelo de datos en el backend

TypeScript permite definir interfaces precisas para los datos. Crea el archivo src/types/todo.ts:

import { Document } from "mongoose";

export interface ITodo extends Document {
    name: string;
    description: string;
    status: boolean;
}

Ahora, define el esquema y modelo en src/models/todo.ts:

import { Schema, model } from "mongoose";
import { ITodo } from "../types/todo";

const todoSchema: Schema<ITodo> = new Schema(
    {
        name: { type: String, required: true },
        description: { type: String, required: true },
        status: { type: Boolean, required: true, default: false },
    },
    { timestamps: true }
);

export default model<ITodo>("Todo", todoSchema);

El uso de timestamps agrega automáticamente campos createdAt y updatedAt, útil para auditoría.

Implementación de controladores para operaciones CRUD

Los controladores manejan la lógica de negocio. En src/controllers/todos/index.ts, implementa las funciones asíncronas:

import { Request, Response } from "express";
import Todo from "../../models/todo";
import { ITodo } from "../../types/todo";

export const getTodos = async (req: Request, res: Response): Promise<void> => {
    try {
        const todos: ITodo[] = await Todo.find();
        res.status(200).json({ todos });
    } catch (error) {
        res.status(500).json({ message: "Error al obtener tareas" });
    }
};

export const addTodo = async (req: Request, res: Response): Promise<void> => {
    try {
        const {
            name,
            description,
            status,
        }: Pick<ITodo, "name" | "description" | "status"> = req.body;
        const newTodo = new Todo({ name, description, status });
        await newTodo.save();
        const todos = await Todo.find();
        res.status(201).json({
            message: "Tarea agregada",
            todo: newTodo,
            todos,
        });
    } catch (error) {
        res.status(500).json({ message: "Error al agregar tarea" });
    }
};

export const updateTodo = async (
    req: Request,
    res: Response
): Promise<void> => {
    try {
        const { id } = req.params;
        const updatedTodo = await Todo.findByIdAndUpdate(id, req.body, {
            new: true,
        });
        const todos = await Todo.find();
        res.status(200).json({
            message: "Tarea actualizada",
            todo: updatedTodo,
            todos,
        });
    } catch (error) {
        res.status(500).json({ message: "Error al actualizar tarea" });
    }
};

export const deleteTodo = async (
    req: Request,
    res: Response
): Promise<void> => {
    try {
        const { id } = req.params;
        await Todo.findByIdAndDelete(id);
        const todos = await Todo.find();
        res.status(200).json({ message: "Tarea eliminada", todos });
    } catch (error) {
        res.status(500).json({ message: "Error al eliminar tarea" });
    }
};

Estas funciones manejan errores de manera centralizada, lo que mejora la robustez de la API REST completa.

Configuración de rutas y servidor principal

En src/routes/index.ts:

import { Router } from "express";
import {
    getTodos,
    addTodo,
    updateTodo,
    deleteTodo,
} from "../controllers/todos";

const router = Router();

router.get("/todos", getTodos);
router.post("/add-todo", addTodo);
router.put("/edit-todo/:id", updateTodo);
router.delete("/delete-todo/:id", deleteTodo);

export default router;

Finalmente, el punto de entrada src/app.ts:

import express from "express";
import cors from "cors";
import mongoose from "mongoose";
import dotenv from "dotenv";
import todoRoutes from "./routes";

dotenv.config();

const app = express();
const PORT = process.env.PORT || 4000;

app.use(cors());
app.use(express.json());
app.use(todoRoutes);

const uri = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`;

mongoose
    .connect(uri)
    .then(() => {
        console.log("Conectado a MongoDB");
        app.listen(PORT, () =>
            console.log(`Servidor corriendo en puerto ${PORT}`)
        );
    })
    .catch((err) => console.error("Error de conexión", err));

Ejecuta yarn dev para iniciar el servidor en modo desarrollo.

Configuración del proyecto frontend con React y TypeScript

El frontend utiliza React con TypeScript. Crea el proyecto:

npx create-vite@latest todo-frontend -- --template react-ts
cd todo-frontend
yarn add axios
yarn dev

Vite es la herramienta recomendada en 2026 por su velocidad y configuración mínima.

Define tipos globales en src/types.ts:

export interface ITodo {
    _id?: string;
    name: string;
    description: string;
    status: boolean;
    createdAt?: string;
    updatedAt?: string;
}

export interface ApiResponse {
    message: string;
    todos: ITodo[];
    todo?: ITodo;
}

Helper para llamadas API con Axios

Crea src/api.ts:

import axios from "axios";
import { ITodo, ApiResponse } from "./types";

const API_URL = "http://localhost:4000";

export const getTodos = () => axios.get<ApiResponse>(`${API_URL}/todos`);

export const addTodo = (todo: Omit<ITodo, "_id">) =>
    axios.post<ApiResponse>(`${API_URL}/add-todo`, todo);

export const updateTodo = (id: string, updates: Partial<ITodo>) =>
    axios.put<ApiResponse>(`${API_URL}/edit-todo/${id}`, updates);

export const deleteTodo = (id: string) =>
    axios.delete<ApiResponse>(`${API_URL}/delete-todo/${id}`);

Este helper centraliza las llamadas y tipa las respuestas, aprovechando las ventajas de TypeScript en el frontend con React.

Componente para agregar nuevas tareas

Crea src/components/AddTodo.tsx:

import React, { useState } from "react";
import { addTodo } from "../api";
import { ITodo } from "../types";

interface Props {
    onAdd: () => void;
}

const AddTodo: React.FC<Props> = ({ onAdd }) => {
    const [name, setName] = useState("");
    const [description, setDescription] = useState("");

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        if (!name || !description) return;
        await addTodo({ name, description, status: false });
        setName("");
        setDescription("");
        onAdd();
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Nombre"
                required
            />
            <input
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                placeholder="Descripción"
                required
            />
            <button type="submit">Agregar tarea</button>
        </form>
    );
};

export default AddTodo;

Componente para mostrar y gestionar tareas individuales

En src/components/TodoItem.tsx:

import React from "react";
import { updateTodo, deleteTodo } from "../api";
import { ITodo } from "../types";

interface Props {
    todo: ITodo;
    onUpdate: () => void;
}

const TodoItem: React.FC<Props> = ({ todo, onUpdate }) => {
    const handleComplete = async () => {
        await updateTodo(todo._id!, { status: !todo.status });
        onUpdate();
    };

    const handleDelete = async () => {
        await deleteTodo(todo._id!);
        onUpdate();
    };

    return (
        <div className={todo.status ? "completed" : ""}>
            <h3>{todo.name}</h3>
            <p>{todo.description}</p>
            <button onClick={handleComplete}>
                {todo.status ? "Desmarcar" : "Completar"}
            </button>
            <button onClick={handleDelete}>Eliminar</button>
        </div>
    );
};

export default TodoItem;

Componente principal App y gestión de estado

En src/App.tsx:

import React, { useEffect, useState } from "react";
import AddTodo from "./components/AddTodo";
import TodoItem from "./components/TodoItem";
import { getTodos } from "./api";
import { ITodo } from "./types";

const App: React.FC = () => {
    const [todos, setTodos] = useState<ITodo[]>([]);

    const fetchTodos = async () => {
        const { data } = await getTodos();
        setTodos(data.todos);
    };

    useEffect(() => {
        fetchTodos();
    }, []);

    return (
        <div>
            <h1>Gestor de Tareas</h1>
            <AddTodo onAdd={fetchTodos} />
            {todos.map((todo) => (
                <TodoItem key={todo._id} todo={todo} onUpdate={fetchTodos} />
            ))}
        </div>
    );
};

export default App;

La recarga de datos tras cada operación asegura que la interfaz refleje siempre el estado actual del servidor.

Mejores prácticas y consideraciones de seguridad

En producción, valida los datos de entrada con bibliotecas como Zod o Joi. Implementa autenticación JWT para proteger las rutas. Usa HTTPS y sanitiza entradas para prevenir inyecciones.

MongoDB Atlas ofrece clusters gratuitos seguros con encriptación en tránsito. Considera índices en campos frecuentemente consultados para mejorar el rendimiento.

En el frontend, maneja estados de carga y errores para una mejor experiencia de usuario.

Conclusiones

La construcción de esta aplicación de tareas demuestra la potencia del stack MERN con TypeScript. La tipificación estricta reduce errores, mientras que la separación clara entre backend y frontend facilita el mantenimiento y la escalabilidad.

Este proyecto sirve como base sólida para aplicaciones más complejas, incorporando características como filtrado, paginación o integración con servicios externos. La combinación de React para interfaces reactivas, Express para APIs robustas y MongoDB para almacenamiento flexible posiciona esta tecnología como una opción viable en el panorama actual del desarrollo web.

Al dominar estos conceptos, los desarrolladores pueden crear soluciones eficientes y modernas, adaptadas a las demandas de proyectos reales en 2026.