├── .gitignore ├── .vscode └── settings.json ├── README.md ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── react.svg └── vercel.svg ├── requests ├── ping.http └── tasks.http ├── src ├── app │ ├── api │ │ ├── ping │ │ │ └── route.js │ │ └── tasks │ │ │ ├── [id] │ │ │ └── route.js │ │ │ └── route.js │ ├── layout.jsx │ ├── not-found.jsx │ ├── page.jsx │ └── tasks │ │ ├── [id] │ │ └── page.jsx │ │ └── new │ │ └── page.jsx ├── components │ ├── Navbar.jsx │ └── TaskCard.jsx ├── models │ ├── Task.js │ └── User.js ├── styles │ └── globals.css └── utils │ └── mongoose.js └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "**/node_modules": true, 10 | } 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextjs & Mongodb CRUD 2 | 3 | A web aplication CRUD using Nodejs y Mongodb (with mongoose) 4 | 5 | ### Installation 6 | 7 | ``` 8 | cd next-mongodb-app 9 | npm i 10 | npm run dev 11 | ``` 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "@/*": [ 6 | "*" 7 | ] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | }; 4 | 5 | module.exports = nextConfig; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-mongodb-tasks", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.4.0", 12 | "bcrypt": "^5.1.0", 13 | "cookie": "^0.5.0", 14 | "formik": "^2.4.2", 15 | "jose": "^4.14.4", 16 | "jsonwebtoken": "^9.0.1", 17 | "mongoose": "^7.4.1", 18 | "morgan": "^1.10.0", 19 | "next": "13.4.12", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "semantic-ui-css": "^2.5.0", 23 | "semantic-ui-react": "^2.1.4" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "18.2.17", 27 | "autoprefixer": "^10.4.14", 28 | "eslint-config-next": "^13.4.12", 29 | "postcss": "^8.4.27", 30 | "tailwindcss": "^3.3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FaztWeb/nextjs-mongodb-crud/911c60adb1d5903e222dff588c9180137493ba30/public/favicon.ico -------------------------------------------------------------------------------- /public/react.svg: -------------------------------------------------------------------------------- 1 | logo -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /requests/ping.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:3000/api/ping -------------------------------------------------------------------------------- /requests/tasks.http: -------------------------------------------------------------------------------- 1 | @api = http://localhost:3000/api/tasks 2 | 3 | ### get tasks 4 | {{api}} 5 | 6 | ### create task 7 | POST {{api}} 8 | Content-Type: application/json 9 | 10 | { 11 | "title": "second task 2", 12 | "description": "second desc" 13 | } 14 | 15 | ### some validation 16 | POST {{api}} 17 | 18 | ### get single task 19 | GET {{api}}/64c347322fb76f285655e882 20 | 21 | ### Update a single task 22 | PUT {{api}}/64c347322fb76f285655e882 23 | Content-Type: application/json 24 | 25 | { 26 | "title": "I have to create a next app" 27 | } 28 | 29 | ### delete a single task 30 | DELETE {{api}}/64c347322fb76f285655e882 -------------------------------------------------------------------------------- /src/app/api/ping/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export function GET() { 4 | return NextResponse.json({ hello: "world" }); 5 | } -------------------------------------------------------------------------------- /src/app/api/tasks/[id]/route.js: -------------------------------------------------------------------------------- 1 | import Task from "@/models/Task"; 2 | import { dbConnect } from "@/utils/mongoose"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function GET(request, { params }) { 6 | dbConnect(); 7 | try { 8 | const taskFound = await Task.findById(params.id); 9 | 10 | if (!taskFound) 11 | return NextResponse.json( 12 | { 13 | message: "Task not found", 14 | }, 15 | { 16 | status: 404, 17 | } 18 | ); 19 | 20 | return NextResponse.json(taskFound); 21 | } catch (error) { 22 | return NextResponse.json(error.message, { 23 | status: 400, 24 | }); 25 | } 26 | } 27 | 28 | export async function PUT(request, { params }) { 29 | const body = await request.json(); 30 | dbConnect(); 31 | 32 | try { 33 | const taskUpdated = await Task.findByIdAndUpdate(params.id, body, { 34 | new: true, 35 | }); 36 | 37 | if (!taskUpdated) 38 | return NextResponse.json( 39 | { 40 | message: "Task not found", 41 | }, 42 | { 43 | status: 404, 44 | } 45 | ); 46 | 47 | return NextResponse.json(taskUpdated); 48 | } catch (error) { 49 | return NextResponse.json(error.message, { 50 | status: 400, 51 | }); 52 | } 53 | } 54 | 55 | export async function DELETE(request, { params }) { 56 | dbConnect(); 57 | 58 | try { 59 | const taskDeleted = await Task.findByIdAndDelete(params.id); 60 | 61 | if (!taskDeleted) 62 | return NextResponse.json( 63 | { 64 | message: "Task not found", 65 | }, 66 | { 67 | status: 404, 68 | } 69 | ); 70 | 71 | return NextResponse.json(taskDeleted); 72 | } catch (error) { 73 | return NextResponse.json(error.message, { 74 | status: 400, 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/api/tasks/route.js: -------------------------------------------------------------------------------- 1 | import Task from "@/models/Task"; 2 | import { dbConnect } from "@/utils/mongoose"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function GET() { 6 | await dbConnect(); 7 | const tasks = await Task.find(); 8 | return NextResponse.json(tasks); 9 | } 10 | 11 | export async function POST(request) { 12 | try { 13 | const body = await request.json(); 14 | const newTask = new Task(body); 15 | const savedTask = await newTask.save(); 16 | return NextResponse.json(savedTask); 17 | } catch (error) { 18 | return NextResponse.json(error.message, { 19 | status: 400, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "components/Navbar"; 2 | import "../styles/globals.css"; 3 | 4 | export const metadata = { 5 | title: "NextMongo", 6 | description: "NextMongo is a simple app to manage tasks.", 7 | } 8 | 9 | function RootLayout({ children }) { 10 | return ( 11 | 12 | 13 | 14 |
{children}
15 | 16 | 17 | ); 18 | } 19 | 20 | export default RootLayout; 21 | -------------------------------------------------------------------------------- /src/app/not-found.jsx: -------------------------------------------------------------------------------- 1 | function NotFound() { 2 | return ( 3 |
4 |

404

5 |

6 | Page not found :( 7 |

8 |
9 | ); 10 | } 11 | 12 | export default NotFound; 13 | -------------------------------------------------------------------------------- /src/app/page.jsx: -------------------------------------------------------------------------------- 1 | import { dbConnect } from "@/utils/mongoose"; 2 | import TaskCard from "@/components/TaskCard"; 3 | import Task from "@/models/Task"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export async function loadTasks() { 8 | await dbConnect(); 9 | const tasks = await Task.find(); 10 | return tasks; 11 | } 12 | 13 | export default async function HomePage() { 14 | const tasks = await loadTasks(); 15 | 16 | return ( 17 |
18 | {tasks.map((task) => ( 19 | 20 | ))} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/tasks/[id]/page.jsx: -------------------------------------------------------------------------------- 1 | import NewPage from "../new/page"; 2 | export default NewPage; 3 | -------------------------------------------------------------------------------- /src/app/tasks/new/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from "react"; 3 | import { useRouter, useParams } from "next/navigation"; 4 | 5 | const NewTask = () => { 6 | const [newTask, setNewTask] = useState({ 7 | title: "", 8 | description: "", 9 | }); 10 | const params = useParams(); 11 | const router = useRouter(); 12 | 13 | const [isSubmitting, setIsSubmitting] = useState(false); 14 | const [errors, setErrors] = useState({}); 15 | 16 | const getTask = async () => { 17 | const res = await fetch(`/api/tasks/${params.id}`); 18 | const data = await res.json(); 19 | setNewTask({ title: data.title, description: data.description }); 20 | }; 21 | 22 | useEffect(() => { 23 | if (params.id) { 24 | getTask(); 25 | } 26 | }, []); 27 | 28 | const handleSubmit = async (e) => { 29 | e.preventDefault(); 30 | let errs = validate(); 31 | 32 | if (Object.keys(errs).length) return setErrors(errs); 33 | 34 | setIsSubmitting(true); 35 | 36 | if (params.id) { 37 | await updateTask(); 38 | } else { 39 | await createTask(); 40 | } 41 | 42 | router.push("/"); 43 | }; 44 | 45 | const handleChange = (e) => 46 | setNewTask({ ...newTask, [e.target.name]: e.target.value }); 47 | 48 | const validate = () => { 49 | let errors = {}; 50 | 51 | if (!newTask.title) { 52 | errors.title = "Title is required"; 53 | } 54 | if (!newTask.description) { 55 | errors.description = "Description is required"; 56 | } 57 | 58 | return errors; 59 | }; 60 | 61 | const createTask = async () => { 62 | try { 63 | await fetch("/api/tasks", { 64 | method: "POST", 65 | headers: { 66 | "Content-Type": "application/json", 67 | }, 68 | body: JSON.stringify(newTask), 69 | }); 70 | router.push("/"); 71 | router.refresh(); 72 | } catch (error) { 73 | console.error(error); 74 | } 75 | }; 76 | 77 | const handleDelete = async () => { 78 | if (window.confirm("Are you sure you want to delete this task?")) { 79 | try { 80 | const res = await fetch(`/api/tasks/${params.id}`, { 81 | method: "DELETE", 82 | }); 83 | router.push("/"); 84 | router.refresh(); 85 | } catch (error) { 86 | console.error(error); 87 | } 88 | } 89 | }; 90 | 91 | const updateTask = async () => { 92 | try { 93 | await fetch(`/api/tasks/${params.id}`, { 94 | method: "PUT", 95 | headers: { 96 | "Content-Type": "application/json", 97 | }, 98 | body: JSON.stringify(newTask), 99 | }); 100 | router.push("/"); 101 | router.refresh(); 102 | } catch (error) { 103 | console.error(error); 104 | } 105 | }; 106 | 107 | return ( 108 |
109 |
110 |
111 |

112 | {!params.id ? "Create Task" : "Update task"} 113 |

114 | {params.id && ( 115 | 121 | )} 122 |
123 | 132 | 133 | 141 | 142 | 145 |
146 |
147 | ); 148 | }; 149 | 150 | export default NewTask; 151 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export const Navbar = () => { 4 | return ( 5 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/TaskCard.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export function TaskCard({ task }) { 4 | return ( 5 | 6 |
7 |

{task.title}

8 |

{task.description}

9 |

10 | Created at: 11 | {new Date(task.createdAt).toLocaleDateString()} 12 |

13 |
14 | 15 | ); 16 | } 17 | 18 | export default TaskCard; 19 | -------------------------------------------------------------------------------- /src/models/Task.js: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const TaskSchema = new Schema( 4 | { 5 | title: { 6 | type: String, 7 | required: [true, "The Task title is required "], 8 | unique: true, 9 | trim: true, 10 | maxlength: [40, "title cannot be grater than 40 characters"], 11 | }, 12 | description: { 13 | type: String, 14 | required: true, 15 | trim: true, 16 | maxlength: [200, "title cannot be grater than 200 characters"], 17 | }, 18 | }, 19 | { 20 | timestamps: true, 21 | versionKey: false, 22 | } 23 | ); 24 | 25 | export default models.Task || model("Task", TaskSchema); 26 | -------------------------------------------------------------------------------- /src/models/User.js: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | import bcrypt from "bcrypt"; 3 | import jwt from "jsonwebtoken"; 4 | 5 | const userSchema = new Schema( 6 | { 7 | name: String, 8 | lastname: String, 9 | email: String, 10 | password: String, 11 | }, 12 | { 13 | timestamps: true, 14 | } 15 | ); 16 | 17 | userSchema.methods.encryptPassword = async function () { 18 | const salt = await bcrypt.genSalt(10); 19 | this.password = await bcrypt.hash(this.password, salt); 20 | }; 21 | 22 | userSchema.statics.comparePassword = async function (password, hash) { 23 | return await bcrypt.compare(password, hash); 24 | }; 25 | 26 | userSchema.methods.generateToken = function () { 27 | return jwt.sign({ id: this._id }, process.env.JWT_SECRET); 28 | }; 29 | 30 | export default models.User || model("User", userSchema); 31 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply bg-gray-900; 7 | color: white; 8 | } -------------------------------------------------------------------------------- /src/utils/mongoose.js: -------------------------------------------------------------------------------- 1 | import { connect, connection } from "mongoose"; 2 | 3 | const conn = { 4 | isConnected: false, 5 | }; 6 | 7 | export async function dbConnect() { 8 | if (conn.isConnected) { 9 | return; 10 | } 11 | 12 | const db = await connect( 13 | process.env.MONGODB_URI || "mongodb://localhost:27017/nextjs" 14 | ); 15 | // console.log(db.connection.db.databaseName); 16 | conn.isConnected = db.connections[0].readyState; 17 | } 18 | 19 | connection.on("connected", () => console.log("Mongodb connected to db")); 20 | 21 | connection.on("error", (err) => console.error("Mongodb Errro:", err.message)); 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | --------------------------------------------------------------------------------