# 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.
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} />;
}