# MongoDB + SSR + SWR

Vamos a conocer como trabajar con SSR y SWR, conectándonos a MongoDB a través del Mongoose.

# Github (código final)

# Requisitos

Conocer las bases de Node.js + MongoDB

# Mongoose

Mongoose proporciona una solución sencilla basada en esquemas para modelar los datos de su aplicación. Incluye conversión de tipos incorporada, validación, creación de consultas, ganchos de lógica empresarial y más, listos para usar.

  • ODM: Object Document Mapper

  • Similar a un ORM (Object Relational Mapper) base de datos relacional.

  • Tanto los ODM como los ORM pueden facilitarle la vida con la estructura y los métodos integrados. La estructura de un ODM u ORM contendrá lógica empresarial que le ayudará a organizar los datos. Los métodos integrados de ODM u ORM automatizan las tareas comunes que le ayudan a comunicarse con los controladores nativos, lo que le ayuda a trabajar de forma más rápida y eficiente.

  • Artículo

  • Getting Started Moongose

npm i mongoose
npm i bootstrap (opcional)

_app.js

import "bootstrap/dist/css/bootstrap.min.css";

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

lib/dbConnect.js

import mongoose from 'mongoose'

const MONGODB_URI = process.env.MONGODB_URI

if (!MONGODB_URI) {
  throw new Error(
    'Please define the MONGODB_URI environment variable inside .env.local'
  )
}

/**
 * Global is used here to maintain a cached connection across hot reloads
 * in development. This prevents connections growing exponentially
 * during API Route usage.
 */
let cached = global.mongoose

if (!cached) {
  cached = global.mongoose = { conn: null, promise: null }
}

async function dbConnect() {
  if (cached.conn) {
    return cached.conn
  }

  if (!cached.promise) {
    const opts = {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      bufferCommands: false,
      bufferMaxEntries: 0,
      useFindAndModify: false,
      useCreateIndex: true,
    }

    cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
      return mongoose
    })
  }
  cached.conn = await cached.promise
  return cached.conn
}

export default dbConnect

.env.local

MONGODB_URI=mongodb+srv://<USER>:<PASS>@cluster0.ncdk5.mongodb.net/<DB_NAME>?retryWrites=true&w=majority

models/Movie.js

import mongoose from "mongoose";
const MovieSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, "Por favor ingrese título"],
  },
  plot: {
    type: String,
    required: [true, "Por favor ingrese descripción"],
  },
});

export default mongoose.models.Movie || mongoose.model("Movie", MovieSchema);

index.jsx

import Head from "next/head";
import dbConnect from "../lib/dbConnect";
import Movie from "../models/Movie";
import Link from "next/link";

export default function Home({ movies }) {
  console.log(movies);
  return (
    <div>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="container">
        <h1 className="my-3 text-center">Movies</h1>

        <Link href="/new">
          <a className="btn btn-primary w-100 mb-3">Agregar</a>
        </Link>

        {movies.map(({ title, _id, plot }) => (
          <div className="card mb-2" key={_id}>
            <div className="card-body">
              <p className="text-uppercase h5">{title}</p>
              <p className="fw-light">{plot}</p>
              <div>
                <Link href="/[id]/edit" as={`/${_id}/edit`}>
                  <a className="btn btn-warning me-2">Editar</a>
                </Link>
                <Link href="/[id]" as={`/${_id}`}>
                  <a className="btn btn-info">Visualizar</a>
                </Link>
              </div>
            </div>
          </div>
        ))}
      </main>
    </div>
  );
}

export async function getServerSideProps() {
  try {
    await dbConnect();
    const result = await Movie.find({});
    const movies = result.map((doc) => {
      const movie = doc.toObject();
      movie._id = movie._id.toString();
      return movie;
    });
    return { props: { movies } };
  } catch (error) {
    console.log(error);
  }
}

# API

WARNING

No debe usar fetch() para llamar a una ruta API (de next.js) en getServerSideProps.

En su lugar, importe directamente la lógica utilizada dentro de su ruta API. Es posible que deba refactorizar ligeramente su código para este enfoque.

¡Recuperar desde una API externa está bien!

api/movie

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

import dbConnect from "../../../lib/dbConnect";
import Movie from "../../../models/Movie";

export default async function handler(req, res) {
  const { method } = req;
  await dbConnect();

  switch (method) {
    case "GET":
      try {
        const movies = await Movie.find({});
        res.status(200).json({ success: true, data: movies });
      } catch (error) {
        res.status(400).json({ success: false });
      }
      break;

    case "POST":
      try {
        const movie = await Movie.create(req.body);
        res.status(200).json({ success: true, data: movie });
      } catch (error) {
        res.status(400).json({ success: false, error });
      }
      break;
    default:
      res.status(400).json({ success: false });
      break;
  }
}

api/movie/[id].js

import dbConnect from "../../../lib/dbConnect";
import Movie from "../../../models/Movie";

export default async function handler(req, res) {
  const {
    method,
    query: { id },
  } = req;
  await dbConnect();

  switch (method) {
    case "GET":
      try {
        const movie = await Movie.findById(id);

        if (!movie) {
          return res.status(400).json({ success: false });
        }
        res.status(200).json({ success: true, data: movie });
      } catch (error) {
        res.status(400).json({ success: false });
      }
      break;

    case "PUT":
      try {
        const movie = await Movie.findByIdAndUpdate(id, req.body, {
          new: true,
          runValidators: true,
        });
        if (!movie) {
          return res.status(400).json({ success: false });
        }
        res.status(200).json({ success: true, data: movie });
      } catch (error) {
        res.status(400).json({ success: false });
      }
      break;

    case "DELETE":
      try {
        const movie = await Movie.deleteOne({ _id: id });
        if (!movie) {
          return res.status(400).json({ success: false });
        }
        res.status(200).json({ success: true, data: {} });
      } catch (error) {
        res.status(400).json({ success: false });
      }
      break;
    default:
      res.status(400).json({ success: false });
      break;
  }
}

# new.jsx

import Form from "../components/Form";

export default function New() {
  const movieForm = {
    title: "",
    plot: "",
  };

  return <Form movieForm={movieForm} />;
}

components/Form.jsx

import { useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { mutate } from "swr";

export default function Form({ forNewMovie = true, movieForm }) {
  const router = useRouter();
  const [message, setMessage] = useState("");
  const [form, setForm] = useState({
    title: movieForm.title,
    plot: movieForm.plot,
  });

  const putData = async (form) => {
    const { id } = router.query;
    try {
      const res = await fetch(`/api/movie/${id}`, {
        method: "PUT",
        headers: {
          "Content-type": "application/json",
        },
        body: JSON.stringify(form),
      });
      if (!res.ok) {
        throw new Error(res.status);
      }

      const { data } = await res.json();
      mutate(`/api/movie/${id}`, data, false);
      router.push("/");
    } catch (error) {
      setMessage("Falló la edición");
    }
  };

  const postData = async (form) => {
    try {
      console.log(form);
      const res = await fetch("/api/movie", {
        method: "POST",
        headers: {
          "Content-type": "application/json",
        },
        body: JSON.stringify(form),
      });

      console.log(res);
      if (!res.ok) {
        throw new Error(res.status);
      }

      router.push("/");
    } catch (error) {
      setMessage("falló crear movie");
    }
  };

  const handleChange = (e) => {
    const target = e.target;
    const value = target.value;
    const name = target.name;
    setForm({
      ...form,
      [name]: value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    forNewMovie ? postData(form) : putData(form);
  };

  return (
    <div className="container">
      <h1 className="my-3">Crear nueva movie</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="title"
          placeholder="Título"
          value={form.title}
          onChange={handleChange}
          autoComplete="off"
          className="form-control my-2"
        />
        <input
          type="text"
          name="plot"
          placeholder="Descripción"
          value={form.plot}
          onChange={handleChange}
          autoComplete="off"
          className="form-control my-2"
        />
        <button
          type="submit"
          disabled={!form.title || !form.plot}
          className="btn btn-primary w-100"
        >
          {forNewMovie ? "Agregar" : "Editar"}
        </button>
        <Link href="/">
          <a className="btn btn-warning w-100 my-3">Volver</a>
        </Link>
        <p>{message}</p>
      </form>
    </div>
  );
}

# index.jsx (home)

import Head from "next/head";
import dbConnect from "../lib/dbConnect";
import Movie from "../models/Movie";
import Link from "next/link";

export default function Home({ movies }) {
  console.log(movies);
  return (
    <div>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="container">
        <h1 className="my-3 text-center">Movies</h1>

        <Link href="/new">
          <a className="btn btn-primary w-100 mb-3">Agregar</a>
        </Link>

        {movies.map(({ title, _id, plot }) => (
          <div className="card mb-2" key={_id}>
            <div className="card-body">
              <p className="text-uppercase h5">{title}</p>
              <p className="fw-light">{plot}</p>
              <div>
                <Link href="/[id]/edit" as={`/${_id}/edit`}>
                  <a className="btn btn-warning me-2">Editar</a>
                </Link>
                <Link href="/[id]" as={`/${_id}`}>
                  <a className="btn btn-info">Visualizar</a>
                </Link>
              </div>
            </div>
          </div>
        ))}
      </main>
    </div>
  );
}

export async function getServerSideProps() {
  try {
    await dbConnect();
    const result = await Movie.find({});
    const movies = result.map((doc) => {
      const movie = doc.toObject();
      movie._id = movie._id.toString();
      return movie;
    });
    return { props: { movies } };
  } catch (error) {
    console.log(error);
  }
}

# [id]/index.jsx

import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import dbConnect from "../../lib/dbConnect";
import Movie from "../../models/Movie";

export default function Index({ movie }) {
  const router = useRouter();
  const [message, setMessage] = useState("");

  const handleDelete = async () => {
    const movieId = router.query.id;
    try {
      await fetch(`/api/movie/${movieId}`, {
        method: "DELETE",
      });
      router.push("/");
    } catch (error) {
      setMessage("Error al eliminar");
    }
  };

  return (
    <div className="container">
      <h1 className="my-2">Detalle:</h1>
      <div className="card">
        <div className="card-body">
          <div className="card-title text-uppercase">
            <h5>{movie.title}</h5>
          </div>
          <p className="fw-light">{movie.plot}</p>
          <Link href="/[id]/edit" as={`/${movie._id}/edit`}>
            <a className="btn btn-warning me-2">Editar</a>
          </Link>
          <button className="btn btn-danger" onClick={handleDelete}>
            Eliminar
          </button>
          {message && <p>{message}</p>}
        </div>
      </div>
    </div>
  );
}

export async function getServerSideProps({ params }) {
  try {
    await dbConnect();
    // https://mongoosejs.com/docs/tutorials/lean.html
    const movie = await Movie.findById(params.id).lean();
    movie._id = movie._id.toString();
    return { props: { movie } };
  } catch (error) {
    console.log("error", error);
  }
}

# [id]/edit.jsx

import { useRouter } from "next/router";
import useSWR from "swr";
import Form from "../../components/Form";

const fetcher = (url) =>
  fetch(url)
    .then((res) => res.json())
    .then((json) => json.data);

export default function Edit() {
  // solo obtener data para enviar al form
  const router = useRouter();
  const { id } = router.query;
  const { data: movie, error } = useSWR(
    id ? `/api/movie/${id}` : null,
    fetcher
  );

  if (error) return <p className="container my-3">Falló en la carga...</p>;
  if (!movie) return <p className="container my-3">Cargando...</p>;

  const movieForm = {
    title: movie.title,
    plot: movie.plot,
  };

  return <Form movieForm={movieForm} forNewMovie={false} />;
}