23 Eylül 2022 Cuma

Node.js ile Mysql veritabanı üzerinde CRUD operasyonları gerçekleştirme

 Merhaba arkadaşlar bugün sizlere mysql üzerinde node.js ile beraber rest api oluşturacağız. Aslında crud operasyonlarının hepsini katmanları da dahil ederek güvenli ve kullanımı anlaşılır bir sistem yapmaya çalışacağız. 

    Tabiki bu sistem sql injektion saldırılarına karşı açık olan bir yöntem olabilir. Ama asıl amacımız bizim node.js ile rest api oluşturmak. Sonraki yazımız da Mysql orm için kullanılan Sequelize ile rest api oluşturacağız. Orada  model mimarileri oluşturma ve sql cümleciklerini kullanmadan sorgularımızı çalıştırma yoluna gideceğiz. Lafı çok uzatmadan başlayalım bence..


Evet arkadaşlar öncelikle proje klasörünü oluşturmuş olarak sayıyorum sizi vs code üzerinde klasör konumuna cd tutorial-api diyerek konumlandırma işlemlerini tamamlıyoruz daha sonrasında node paketleri için node init -y diyerek package.json dosyasının oluşmasını sağlıyoruz.


Daha sonrasında sırasıyla ihtiyacımız olacak paketleri npm i ... veya npm install ... paketlerin indirilmesi aşamasına geçmiş bulunuyoruz. Alt kısımda ben kullandığım npm paketlerini paylaşacağım.

"dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.1",
    "mysql": "^2.18.1",
    "nodemon": "^2.0.20"
  }

Evet npm paketlerini de kurduk peki sonrasında ne yapıyoruz. Node js çalışabilmesi için server.js veya app.js isim size kalmış dosyamızı oluşturuyoruz.

Ben server.js diye isimlendirdim ve içerisine express server için kodlarımızı yazmaya başlayalım..

 const express = require("express");


const cors = require("cors");

const app = express();

Öncelikle ana kodlarımız require diyerek onları kullanacağımızı belirtiyoruz. 

Express require çünkü bizim için server tanımlaması yapacak.

Cors require çünkü bizim için cors hatalarına engel olacak ve son olarak da ana uygulamamız app diyerek express üzerinde betimlemesini yapmış olacağız.

// parse requests of content-type - application/json
app.use(express.json());
// parse requests of content-type - application/x-www-form-urlencoded
app.use(
  express.urlencoded({ extended: true })
); /* bodyParser.urlencoded() is deprecated */

Daha sonrasında app üzerinde json parse işlemleri için bu parametreyide geçiyoruz.

Parse işlemleri için  gelen methodlara göre tanılamalarımızı atlamıyoruz. Ana dizini de belirtip projenin yayında olduğunu test etmek için yolunu da belirtiyoruz.

// simple route
app.get("/", (req, res) => {
  res.json({ message: "Welcome to tutorial backend application." });
});

Ve uygulamaya controllers kullanacağımızı route.js den gelen istekleri yönetimini yapacağımız dosyayı yani  tutorial.controlller.js dosyasını da app üzerinde require olarak tanımlayıp projenin listen olmasına geçiyoruz.

require("./app/routes/tutorial.routes.js")(app);

Eğer dışarıdan bir port .env dosyasından girilmediyse sana express sunucusuna buradan yayın aç ve dinle demek için server.js dosyasını son eklememezi yapıyoruz.

// set port, listen for requests
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});

Ben buraya kadar projeyi oluşturduktan sonra şu tarz hatalarla karşılaştım. 

Öncelikle nodemon .\server.js komutunu çalıştırdığımda powershell üzerinde yetki düzenlemesi istedi. Onu ufak bir stackoverflow araştırması sonrası hallettim Eğer bu şekilde hata ile karşılaşırsanız diye linki aşağıda bırakacağım.

Problem: Getting “cannot be loaded because running scripts is disabled on this system” error!

  • Set the execution Policy with the following command: Set-ExecutionPolicy RemoteSigned


Link: https://www.sharepointdiary.com/2014/03/fix-for-powershell-script-cannot-be-loaded-because-running-scripts-is-disabled-on-this-system.html


Bunu da halletikten sonra dosya konumuna app diye yeni bir klasör dizini oluşturuyorum. Bunun içerisinde katman olması ve kullanılabilirlik açısından kolay olsun diye Sırasıyla

config - controllers - models - routes  diye 4 adet klasörümü oluşturuyorum.  Projem bittiğinde aşağıdaki gibi bir klasör dizinine sahip oluyorum.

 
Jpg dosyalarına takılmayınız github reposunda readme.md dosyasında kullanılmak için projenin sonunda eklenmiştir.

Evet arkadaşlar sırasıyla klasör yapımızı da oluşturduk şimdi sırada  ./config/db.config.js dosyasına connection bilgilerini girelim.

module.exports = {
  host: <YOUR_HOST>,
  user: "root",
  password: <YOUR_PASSWORD>,
  database: <YOUR_DATABASE>,
};

    Connection için kullanılacak bilgileri de girdikten sonra ./models klasörü altında sorgularımızı ve model yapımızı oluşturalım ve db.js diye dosya üzerinden connection bilgilerini app üzerinde yollayalım. 

const mysql = require("mysql");
const dbConfig = require("../config/db.config.js");

var connection = mysql.createConnection({
  host: dbConfig.host,
  user: dbConfig.user,
  password: dbConfig.password,
  database: dbConfig.database,
});

    db.js üzerinde require olan alanları belirterek kodlamaya başlıyoruz. dbConfig üzerinden connection bilgilerini ve mysql üzerinde çalışma yapacağımız için iki alanı belirtiyoruz.

// open the MySQL connection
connection.connect((error) => {
  if (error) throw error;
  console.log("Successfully connected to the database.");
});

module.exports = connection;

Sonrasında connection oluşturduktan sonra connect prosesini başlatıyoruz ve node.js üzerinden bize başarılı bağlantı mesajının gelmesini bekliyoruz.

     Db.js de bitti bundan sonra her model için sorgularımızı ve methodlarımızın çalışacak olan tutorial.model.js sayfasını doldurmaya geldi. Buradakiler kısmen sql bilgisi içerse de basit sorgular olduğu için  ayrıntıya girmeden geçeceğim.

Öncelikle require alanlarımızı geçiyoruz.

const sql = require("./db.js");

Sonrasında Tutorial diye bir referans alınacak model oluşturuyoruz ki her defasında  controller tarafında  uzun uzadıya model ismini yazmak zorunda kalmayalım.

--> Models alanı

Hadi methodlarımızı yazalım ve controllere doğru geçelim.

Öncelikle Create methodumuz yani veri ekleme işine yarayacak kodlarımızı yazalım.

Tutorial.create = (newTutorial, result) => {
  sql.query("INSERT INTO tutorials SET ?", newTutorial, (err, res) => {
    if (err) {
      console.log("error: ", err);
      result(err, null);
      return;
    }

    console.log("created tutorial: ", { id: res.insertId, ...newTutorial });
    result(null, { id: res.insertId, ...newTutorial });
  });
};

Burada ki require alanımızı db olarak da görebilirsiniz aslında kullanılan connection üzerinde execute yapmamızı sağlamaktır.  O yüzden sql cümleciği diye sql yazdık diye takılmayın.

Tutorial.findById = (id, result) => {
  sql.query(`SELECT * FROM tutorials WHERE id= ${id}`, (err, res) => {
    if (err) {
      console.log("error: ", err);
      result(err, null);
      return;
    }

    if (res.length) {
      console.log("found tutorial: ", res[0]);
      result(null, res[0]);
      return;
    }

    // not found Tutorial with the id
    result({ kind: "not_found" }, null);
  });
};

findById methodunu da aktif ettik burada id kısmında  where=${id} string  interpolation kullandığımızı görebilirsiniz. Merak edenler için string interpolation nedir ?

Sonrasında getAll  ve parametreye bağlı getirmeyi de ekledik. Eğer ki get methodu çalışırken title dolu giderse sistem title nesnesi içerisinde ki geçenlere göre arama yapacak şekilde listeleme yapıyor.

Tutorial.getAll = (title, result) => {
    let query = "SELECT * FROM tutorials";
 
    if (title) {
      query += ` WHERE title LIKE '%${title}%'`;
    }
 
    sql.query(query, (err, res) => {
      if (err) {
        console.log("error: ", err);
        result(null, err);
        return;
      }
 
      console.log("tutorials: ", res);
      result(null, res);
    });
  };

Yoksa da zaten bütün sonuçları geri bize dönderiyor. Son iki model methodumuz kaldı onları da ekleyelim route ve controller sürecine geçelim.

  Tutorial.updateById = (id, tutorial, result) => {
    sql.query(
      "UPDATE tutorials SET title = ?, description = ?, published = ? WHERE id = ?",
      [tutorial.title, tutorial.description, tutorial.published, id],
      (err, res) => {
        if (err) {
          console.log("error: ", err);
          result(null, err);
          return;
        }
 
        if (res.affectedRows == 0) {
          // not found Tutorial with the id
          result({ kind: "not_found" }, null);
          return;
        }
 
        console.log("updated tutorial: ", { id: id, ...tutorial });
        result(null, { id: id, ...tutorial });
      }
    );
  };

Update kısmında bütün alanların güncellenme durumuna bağlı olarak bütün alanları ekliyoruz ve parametre olarak gelen bilgiyi array olarak sql query'e ekliyoruz. Tabi bunşarı yaparken iki farklı hata döndürmeyi de unutmuyoruz. Burada ki hatalara bakarsak aranılan id yoktur veya server'a erişilememiştir. 

Bunları rahatlıkla asıl sorguya geçmeden return edebiliriz. Siz daha çok validate durumu sorgulamak isterseniz bunları asıl sorgunuz öncesinde tanımlama ya da middleware paketlerini kurarak mimarinizi daha da sağlamlaştırabilirsiniz.

Son  model methodu olarak da delete operasyonunu gerçekleştirelim.

  Tutorial.remove = (id, result) => {
    sql.query("DELETE FROM tutorials WHERE id = ?", id, (err, res) => {
      if (err) {
        console.log("error: ", err);
        result(null, err);
        return;
      }
 
      if (res.affectedRows == 0) {
        // not found Tutorial with the id
        result({ kind: "not_found" }, null);
        return;
      }
 
      console.log("deleted tutorial with id: ", id);
      result(null, res);
    });
  };

Aslında burada tek method halinde deleteAll ve deleteById şeklinde yapılabilirdi ama kafa karıştırmamak için birbirinden ayırarak süreci tamamlayalım.

 Tutorial.removeAll = result => {
    sql.query("DELETE FROM tutorials", (err, res) => {
      if (err) {
        console.log("error: ", err);
        result(null, err);
        return;
      }
 
      console.log(`deleted ${res.affectedRows} tutorials`);
      result(null, res);
    });
  };


--> Controller alanı

Burada mantık biraz aslında önceki uygulamalara göre karışabilir. Çünkü iç içe durumlar olacak bu durumları tekrar tekrar uygulayarak özümsemekte fayda var bence. Route ve model işlemlerini aslında burada yönetmeye çalışıyoruz. Route gelen bir isteği model'e gitmeden önce doğru yönlenmesini ve doğru sonucun dönmesine ortam sağlıyoruz.

Require alanımızı da geçtikten sonra;

const Tutorial = require("../models/tutorial.model.js");

Save işlemleri için yapılacak model methodunu controller methodu içerisinde belirtelim.

exports.create = (req, res) => {
  //Validate request
  if (!req.body) {
    res.status(400).send({
      message: "Content can not be empty!",
    });
  }

  // Create a Tutorial
  const tutorial = new Tutorial({
    title: req.body.title,
    description: req.body.description,
    published: req.body.published || false,
  });

  console.log(tutorial);

  // Save Tutorial in the database
  Tutorial.create(tutorial, (err, data) => {
    if (err)
      res.status(500).send({
        message:
          err.message || "Some error occurred while creating the Tutorial.",
      });
    else res.send(data);
  });
};

Validate kontrolü yaparak model içerisinde boş bilgi gidip sistem hatasını engellemeye öncelik veriyoruz. Sonrasında dolu olan req.body içerisinde ki gönderilen requestleri yeni bir json nesnesi haline getirip create methodunun içerisine parametre olarak yolluyoruz.

exports.findAll = (req, res) => {
  const title = req.query.title;

  Tutorial.getAll(title, (err, data) => {
    if (err)
      res.status(500).send({
        message: err.message || "some error occured while retrieving tutorials",
      });
    else res.send(data);
  });
};

Hem title parametresine bağlı olarak filtreleme yada bütün sonuçları getirmesi için methodumuz da bu şekilde tanımlıyoruz.

GetById methodumuz da ekleyelim;

// Find a single Tutorial by Id
exports.findOne = (req, res) => {
  Tutorial.findById(req.params.id, (err, data) => {
    if (err) {
      if (err.kind === "not_found") {
        res.status(404).send({
          message: `Not found Tutorial with id ${req.params.id}.`,
        });
      } else {
        res.status(500).send({
          message: "Error retrieving Tutorial with id " + req.params.id,
        });
      }
    } else res.send(data);
  });
};

Arkadaşlar her method içerisinde validate kontrolü yapmayı unutmayın bu tarz önlemler kısmende olsa injection saldırılarına karşı önlem olabilir. Bu tarz durumlar olmasa bile sistemin ekstra hata fırtlatmasını ve servis tarafını kullanan kullanıcıların doğru yol almasını sağlamış olursunuz.

exports.update = (req, res) => {
  //Validate request
  if (!req.body) {
    res.status(400).send({
      message: "Content can not be empty!",
    });
  }

  Tutorial.updateById(
    req.params.id,
    new Tutorial(req.body),
    (err, data) => {
      if (err) {
        if (err.kind === "not_found") {
          res.status(404).send({
            message: `Not found Tutorial with id ${req.params.id}.`
          });
        } else {
          res.status(500).send({
            message: "Error updating Tutorial with id " + req.params.id
          });
        }
      } else res.send(data);
    }
  );

};

Updated methodundan sonrasında delete kısmınıda ekleyelim süreci tamamlayalım.

// Delete a Tutorial with the specified id in the request
exports.delete = (req, res) => {
    Tutorial.remove(req.params.id, (err, data) => {
      if (err) {
        if (err.kind === "not_found") {
          res.status(404).send({
            message: `Not found Tutorial with id ${req.params.id}.`
          });
        } else {
          res.status(500).send({
            message: "Could not delete Tutorial with id " + req.params.id
          });
        }
      } else res.send({ message: `Tutorial was deleted successfully!` });
    });
  };


Arkadaşlar exports sonrası gelen method isimlerine dikkat edelim onlar bizim route işlemlerinde yol almamızı kolaylaştıracaklar. update get ve delete kısımları routing için yönlendirme sağlayacaklar.

Lafı uzatmadan routing kısmına da geçelim ve projenin postman de ki görütünlerini aktaralım sizlere.

Burada ki require alanımız nereden geldiğine dikkat edelim.

const tutorials = require("../controllers/tutorial.controller.js");

Domain yönlendirmesini de son olarak ekleyip routing işlemlerinide tamamlamış oluyoruz.

module.exports = app => {
    const tutorials = require("../controllers/tutorial.controller.js");

    var router = require("express").Router();

    //create a new tutorial
    router.post("/", tutorials.create);

    //retrieve all tutorials
    router.get("/",tutorials.findAll);

    //retrieve get all puslihed tutorials
    router.get("/published", tutorials.findAllPublished);

    //retrieve a single tutorial with id
    router.get("/:id", tutorials.findOne);

    //update tutorial with id
    router.put("/:id", tutorials.update);

    //delete tutorial with id
    router.delete("/:id", tutorials.delete);

    //delete all tutorials
    router.delete("/", tutorials.deleteAll);

    //domain
    app.use('/api/tutorials', router);

};

Projenin Github reposunu da paylaşalım aradaki anlatımdan kaynaklı kopukluklar ortadan kalkmış olsun. Umarım faydalı bir içerik olmuştur. Ben öğrendiklerimi bu şekilde yazarak geliştirme taraftarıyım. Bir çok kaynaktan veriler tüketiyorum onları da kaynak olarak ekleyeceğim. 

Postman Çıktıları github reposundan resimerli paylaşıyorum;

Get Method Postman Result 

Get Method For Title Postman Result

Post Method For Create Operation Postman Result


Faydalı kayanklar;

Github Reposu: https://github.com/learninfinity/CRUD-Operation-using-NodeJS-ExpressJS-MySQL/blob/master/app.js

Github Reposu: https://github.com/CodAffection/Node.js-MySQL-CRUD-Operations

Github Reposu: https://github.com/indraarianggi/nodejs-mysql-api/tree/master/app