Laravel Türkiye Discord Kanalı Forumda kod paylaşılırken dikkat edilmesi gerekenler!Birlikte proje geliştirmek ister misiniz?

Selam, ben frontend (nuxt 3) ve backend (nodejs) de proje yapıyorum kodlarım şöyle.

Backend

utils/fileUpload.js

import multer from "multer";
import path from "path";

const storage = multer.diskStorage({
   destination: (req, file, cb) => {
      cb(null, 'uploads');
   },
   filename: (req, file, cb) => {
      cb(null, Date.now() + path.extname(file.originalname));
      // cb(null, `${new Date().toISOString().replace(/:/g, '-')}${file.originalname}`);
   }
});

const upload = multer({
   storage,
   fileFilter: (req, file, cb) => {
      if(file.mimetype === 'image/png' || file.mimetype === 'image/jpg' || file.mimetype === 'image/jpeg' || file.mimetype === 'image/gif') {
         cb(null, true);     
      }else {
         cb(new Error('MimeType not supported'), false);
      }
   },
//    limits: {
//       fileSize: 1024 * 1024 * 5
//   },
});

export default upload;

bookRoute.js

router.post('/upload', authMiddleware.authenticateUser, upload.single('image'), bookController.uploadFile);

bookController.js

const uploadFile = async (req, res) => {
   try {
      if (!req.file) {
         return res.status(400).json({ error: 'No file uploaded' });
      }

      const filePath = req.file.path;
      res.status(200).json({ message: 'File uploaded successfully', filePath });
   } catch (error) {
      console.error('Error uploading file', error);
      res.status(500).json({ error: 'Internal Server ERROR' });
   }
};
const store = async (req, res) => {
   try {

      upload.single('image')(req, res, async (err) => {
         if (err) {
            return res.status(400).json({ error: err.message });
         }

         const { name, author, description, page } = req.body;
         const uploader = req.user._id;
         const image = req.file ? req.file.path : null;

         console.log('Uploaded file:', req.file);
         console.log('Image path:', image || 'No file');

         if (!image) { // Check if image was uploaded
            return res.status(400).json({ error: 'Image upload failed.' });
         }


         const existingBook = await Book.findOne({ name, author });

         if (existingBook) {
            return res.status(400).json({ error: 'A book with same name and author already exist!' });
         };

         const newBook = await Book.create({
            name,
            author,
            description,
            page,
            uploader,
            image
         });

         return res.status(201).json({
            message: 'Book created successfully',
            books: newBook
         });

      });

   } catch (error) {
      // Handle validation errors
      if (error.name === 'ValidationError') {
         if (checkValidationErrors(error, res)) return;
      } else {
         console.error("Error at creating book", error);
         return res
            .status(500)
            .json({ error: 'Internal Server ERROR' });
      }
   }
};

Frontend (Nuxt 3)

DashboardBooks.vue

<script setup lang="ts">
import type { Modal } from "bootstrap";
import { Dropzone } from "dropzone";
import "dropzone/dist/dropzone.css";
// import VueDropzone from 'vue3-dropzone';
import { useToast } from "vue-toastification";
import { useBookStore } from "~/store/bookStore";
import { useAuthStore } from "~/store/authStore";
import PaginationWidget from "../widgets/PaginationWidget.vue";
import type { Book } from "~/types";

// Use Nuxt App and Book Store
const { $bootstrap } = useNuxtApp();
const toast = useToast();
const bookStore = useBookStore();
const authStore = useAuthStore();

// Reactive state for the new book
let newBook = reactive<Book>({
  name: "",
  author: "",
  description: "",
  page: null,
  editedBookId: null,
});
const modalTitle = ref<string>("Add Book");
const currentPage = ref<number>(1);
const itemsPerPage = ref<number>(2);
const dropzoneElement = ref<HTMLDivElement | null>(null);

let modal: Modal | undefined;
let dropzone: Dropzone | null = null;

// Methods
const saveBook = () => {
  modalTitle.value === "Add Book" ? addBook() : editBook();
};

const updatePage = (page: number) => {
  currentPage.value = page;
};

const openAddModal = () => {
  modalTitle.value = "Add Book";
  Object.assign(newBook, {
    name: "",
    author: "",
    description: "",
    page: null,
    editedBook: null,
  });
  modal?.show();
};

const openEditModal = (existingBook: Book) => {
  modalTitle.value = "Edit Book";
  Object.assign(newBook, {
    ...existingBook,
    editedBookId: existingBook._id,
  });
  modal?.show();
};

const addBook = async () => {
  try {
    await bookStore.addBook(newBook);
    currentPage.value = 1;
    modal?.hide();
    Object.assign(newBook, {
      name: "",
      author: "",
      description: "",
      page: null,
      editedBookId: null,
    });

    await bookStore.fetchBooksByUploader();

    showToast("New book added successfully", {
      type: "success",
      position: "top-right",
      timeout: 1000,
    });
  } catch (error) {
    console.log(error);
  }
};

const editBook = async () => {
  try {
    await bookStore.editTheBook(newBook.editedBookId, newBook);
    await bookStore.fetchBooksByUploader();

    modal?.hide();
    showToast("The book edited successfully", {
      type: "success",
      timeout: 3000,
    });
  } catch (error) {
    console.error(error);
  }
};

const showToast = (message: string, options: object) => {
  toast(message, {
    position: "top-right",
    closeButton: "button",
    icon: true,
    rtl: false,
    ...options,
  });
};

const deleteBook = async (id: string, name: string) => {
  try {
    await bookStore.deleteTheBook(id);

    await bookStore.fetchBooksByUploader();

    showToast(`${name} deleted successfully`, {
      type: "warning",
      timeout: 3000,
    });
  } catch (error) {
    console.error(error);
  }
};

// Computed property for user books
const userBooks = computed(() => {
  return bookStore.userUploadedBooks.slice().sort((a: Book, b: Book) => {
    const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
    const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
    return dateB - dateA;
  });
});

const totalPages = computed(() => {
  return Math.ceil(userBooks.value.length / itemsPerPage.value);
});

const paginatedBooks = computed(() => {
  const startIndex = (currentPage.value - 1) * itemsPerPage.value;
  const endIndex = startIndex + itemsPerPage.value;
  return userBooks.value.slice(startIndex, endIndex);
});

// Lifecycle hook
onMounted(() => {
  const modalElement = document.getElementById("modal-main");
  if (modalElement) {
    modal = new $bootstrap.Modal(modalElement);
  }

  bookStore.fetchBooksByUploader();

  if (dropzoneElement.value) {
    dropzone = new Dropzone(dropzoneElement.value, {
      url: "http://localhost:5000/api/v1/books/upload",
      thumbnailWidth: 150,
      maxFilesize: 2, // Max file size in MB
      dictDefaultMessage: "Drag files here or click to upload",
      paramName: "image",
      acceptedFiles: "image/*",
      init: function () {
        this.on("sending", (file, xhr, formData) => {
          xhr.setRequestHeader("Authorization", `Bearer ${authStore.token}`);
        });

        this.on("success", (file, response) => {
          // Handle success
          // if (response.filePath) {
          //   newBook.image = response.filePath;
            console.log("File uploaded successfully", response);
          // } else {
          //   console.error("No file path returned.");
          // }
        });

        this.on("error", (file, message) => {
          // Handle error
          console.error("Upload failed:", message);
        });
      },
    });
  }
});

onUnmounted(() => {
  if (dropzone) {
    dropzone.destroy(); // Clean up Dropzone instance when component unmounts
  }
});
</script>

<template>
  <div>
    <!-- Button -->
    <div class="row mb-3">
      <div class="col text-end">
        <button type="button" class="btn btn-primary" @click="openAddModal()">
          Add Book
        </button>
      </div>
    </div>

    <!-- Table -->
    <div class="row">
      <div class="col">
        <table class="table">
          <thead>
            <tr>
              <th>Title</th>
              <th>Author</th>
              <th>Description</th>
              <th>Page</th>
              <th class="text-center">Edit</th>
              <th class="text-center">Delete</th>
            </tr>
          </thead>
          <TransitionGroup name="list" tag="tbody">
            <tr v-for="book in paginatedBooks" :key="book._id">
              <td>{{ book.name }}</td>
              <td>{{ book.author }}</td>
              <td style="max-width: 250px">
                {{ book.description }}
              </td>
              <td>{{ book.page }}</td>
              <td class="text-center">
                <font-awesome
                  :icon="['far', 'pen-to-square']"
                  class="text-warning"
                  style="cursor: pointer"
                  @click="openEditModal(book)"
                />
              </td>
              <td class="text-center">
                <font-awesome
                  :icon="['fas', 'trash']"
                  class="text-danger"
                  style="cursor: pointer"
                  @click="deleteBook(book._id, book.name)"
                />
              </td>
            </tr>
          </TransitionGroup>
        </table>
      </div>
    </div>

    <div class="row">
      <PaginationWidget
        :currentPage="currentPage"
        :totalPages="totalPages"
        @page-changed="updatePage"
      />
    </div>

    <!-- Modal -->
    <div class="modal fade" id="modal-main" tabindex="-1" aria-hidden="true">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="addModalLabel">{{ modalTitle }}</h5>
            <button
              type="button"
              @click="modal.hide()"
              class="btn-close"
              aria-label="Close"
            ></button>
          </div>
          <div class="modal-body mx-5">
            <div class="col mb-3">
              <label for="title" class="form-label"
                >Name
                <span class="text-danger">*</span>
              </label>
              <input
                type="text"
                class="form-control"
                id="name"
                name="name"
                v-model="newBook.name"
                required
              />
            </div>
            <div class="col mb-3">
              <label for="author" class="form-label"
                >Author
                <span class="text-danger">*</span>
              </label>
              <input
                type="text"
                class="form-control"
                id="author"
                name="author"
                v-model="newBook.author"
                required
              />
            </div>
            <div class="col mb-3">
              <label for="description" class="form-label"
                >Description
                <span class="text-danger">*</span>
              </label>
              <textarea
                name="description"
                id="description"
                class="form-control"
                cols="30"
                rows="4"
                v-model="newBook.description"
              ></textarea>
            </div>
            <div class="col mb-3">
              <label for="author" class="form-label"
                >Number of Pages
                <span class="text-danger">*</span>
              </label>
              <input
                type="number"
                class="form-control"
                id="numOfPages"
                name="numOfPages"
                v-model="newBook.page"
                required
              />
            </div>

            <div ref="dropzoneElement" class="my-dropzone"></div>

            <div class="text-end mb-4">
              <button
                @click="modal.hide()"
                type="button"
                class="btn btn-outline-secondary"
              >
                Close
              </button>
              <button @click="saveBook()" type="button" class="btn btn-primary">
                Save
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

bu kodlardakı önemli kısım

onMounted(() => {
  const modalElement = document.getElementById("modal-main");
  if (modalElement) {
    modal = new $bootstrap.Modal(modalElement);
  }

  bookStore.fetchBooksByUploader();

  if (dropzoneElement.value) {
    dropzone = new Dropzone(dropzoneElement.value, {
      url: "http://localhost:5000/api/v1/books/upload",
      thumbnailWidth: 150,
      maxFilesize: 2, // Max file size in MB
      dictDefaultMessage: "Drag files here or click to upload",
      paramName: "image",
      acceptedFiles: "image/*",
      init: function () {
        this.on("sending", (file, xhr, formData) => {
          xhr.setRequestHeader("Authorization", `Bearer ${authStore.token}`);
        });

        this.on("success", (file, response) => {
          // Handle success
          // if (response.filePath) {
          //   newBook.image = response.filePath;
            console.log("File uploaded successfully", response);
          // } else {
          //   console.error("No file path returned.");
          // }
        });

        this.on("error", (file, message) => {
          // Handle error
          console.error("Upload failed:", message);
        });
      },
    });
  }
});

Foto yükler yüklemez consolda

File uploaded successfully {message: 'File uploaded successfully', filePath: 'uploads/1725613201618.jpg'}

geliyor oda

console.log("File uploaded successfully", response);

buradan gelyor

böylece resimi backendde olan uploads klasörüne yüklemiş oluyor fakat. Submit yaptığımda (yani save). Console da hata çıkıyor

POST http://127.0.0.1:5000/api/v1/books 400 (Bad Request)
Request URL: http://127.0.0.1:5000/api/v1/books
Request Method: POST
Status Code: 400 Bad Request
Remote Address: 127.0.0.1:5000
Referrer Policy: strict-origin-when-cross-origin

{
    "name": "Hilary Slater",
    "author": "567",
    "description": "Magna sunt do volupt",
    "page": 36,
    "editedBookId": null,
    "editedBook": null
}

{
    "error": "Image upload failed."
}

bu frontend tarafındakı hatalar.

backend tarafında ise

console.log('Uploaded file:', req.file);
console.log('Image path:', image || 'No file');

bunlardan

Server listening on 5000
Uploaded file: undefined
Image path: No file

böyle hata geliyor. Ve ben database image yolunu yazamıyorum.

Gariptir

const uploadFile = async (req, res) => {
   try {
      if (!req.file) {
         return res.status(400).json({ error: 'No file uploaded' });
      }

      console.log(req.file.path, 'REQ FILE PATH');
      

      const filePath = req.file.path;
      res.status(200).json({ message: 'File uploaded successfully', filePath });
   } catch (error) {
      console.error('Error uploading file', error);
      res.status(500).json({ error: 'Internal Server ERROR' });
   }
};

burada req.file geliyor
uploads/1725618211152.jpg REQ FILE PATH

ama store da gelmiyor. Route

router
   .route('/')
   .get(bookController.index)
   .post(authMiddleware.authenticateUser, upload.single('image'), bookController.store);

böyle yaptım belki olur ama yine olmadı.

DÜZELTME

 formData.append("name", newBook.name);
          formData.append("author", newBook.author);
          formData.append("description", newBook.description);
          formData.append("page", newBook.page);

böyle düzelttim fakat bu seferde

A book with same name and author already exist! hatası alıyorum

bookRoute

router.post('/upload', authMiddleware.authenticateUser, upload.single('image'), bookController.uploadFile);

router
   .route('/uploader')
   .get(authMiddleware.authenticateUser, bookController.getBooksByUploader);

bookController

const uploadFile = async (req, res) => {
   try {
      if (!req.file) {
         return res.status(400).json({ error: 'No file uploaded' });
      }

      const filePath = req.file.path;
      res.status(200).json({ message: 'File uploaded successfully', filePath });
   } catch (error) {
      console.error('Error uploading file', error);
      res.status(500).json({ error: 'Internal Server ERROR' });
   }
};
const store = async (req, res) => {
   try {
      const { name, author, description, page, image } = req.body;
      const uploader = req.user._id;

      const existingBook = await Book.findOne({ name, author });

      if (existingBook) {
         return res.status(400).json({ error: 'A book with same name and author already exist!' });
      };

      // const image = req.file ? req.file.path : null;
      console.log('Image path:', image || 'No file');

      const newBook = await Book.create({
         name,
         author,
         description,
         page,
         uploader,
         image
      });

      return res.status(201).json({
         message: 'Book created successfully',
         books: newBook
      });

   } catch (error) {
      // Handle validation errors
      if (error.name === 'ValidationError') {
         if (checkValidationErrors(error, res)) return;
      } else {
         console.error("Error at creating book", error);
         return res
            .status(500)
            .json({ error: 'Internal Server ERROR' });
      }
   }
};
onMounted(() => {
  const modalElement = document.getElementById("modal-main");
  if (modalElement) {
    modal = new $bootstrap.Modal(modalElement);
  }

  bookStore.fetchBooksByUploader();

  if (dropzoneElement.value) {
    dropzone = new Dropzone(dropzoneElement.value, {
      url: "http://localhost:5000/api/v1/books/upload",
      thumbnailWidth: 150,
      maxFilesize: 2, // Max file size in MB
      dictDefaultMessage: "Drag files here or click to upload",
      paramName: "image", // Param name should match with multer's expected field name
      acceptedFiles: "image/*",
      autoProcessQueue: false, // Disable automatic file uploads
      init: function () {
        this.on("sending", (file, xhr, formData) => {
          xhr.setRequestHeader("Authorization", `Bearer ${authStore.token}`);

          // formData.append("name", newBook.name);
          // formData.append("author", newBook.author);
          // formData.append("description", newBook.description);
          // formData.append("page", newBook.page);
        });

        this.on("success", (file, response) => {
          // Handle success
          console.log("File uploaded successfully", response);
          newBook.image = response.filePath;
        });

        this.on("error", (file, message) => {
          // Handle error
          console.error("Upload failed:", message);
        });
      },
    });
  }
});
const addBook = async () => {
  try {
    await new Promise((resolve, reject) => {
      dropzone.on("complete", (file) => {
        if (file.status === 'success') {
          resolve();
        } else {
          reject(new Error("File upload failed"));
        }
      });

      dropzone.processQueue();
    });

    await bookStore.addBook(newBook);

    currentPage.value = 1;
    modal?.hide();
    Object.assign(newBook, {
      name: "",
      author: "",
      description: "",
      page: null,
      image: null,
      editedBookId: null,
    });

    await bookStore.fetchBooksByUploader();

    showToast("New book added successfully", {
      type: "success",
      position: "top-right",
      timeout: 1000,
    });
  } catch (error) {
    console.log(error);
    showToast("Failed to add book", {
      type: "error",
      position: "top-right",
      timeout: 3000,
    });
  }
};

Böyle yaptım fakat bir resimin yolunu

const { name, author, description, page, image } = req.body;

böyle değil req.file ile kaydetmek gerekirdi sanki öyle değil mi?

@mgsmus hocam affedersiniz rahatsız ediyorum ama sizce req.file ile değilde ben body ile yolladım o doğru mu değil mi?