<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Satyam Kumar</title>
    <description>The latest articles on Forem by Satyam Kumar (@satyamkumarsingh).</description>
    <link>https://forem.com/satyamkumarsingh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1886032%2F95e58f04-b189-40a2-af99-2bd4cd7b916d.png</url>
      <title>Forem: Satyam Kumar</title>
      <link>https://forem.com/satyamkumarsingh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/satyamkumarsingh"/>
    <language>en</language>
    <item>
      <title>Building an API for Uploading Images to Google Cloud Storage and Storing Survey Data in PostgreSQL using node</title>
      <dc:creator>Satyam Kumar</dc:creator>
      <pubDate>Thu, 15 Aug 2024 11:52:01 +0000</pubDate>
      <link>https://forem.com/satyamkumarsingh/building-an-api-for-uploading-images-to-google-cloud-storage-and-storing-survey-data-in-postgresql-using-node-cp2</link>
      <guid>https://forem.com/satyamkumarsingh/building-an-api-for-uploading-images-to-google-cloud-storage-and-storing-survey-data-in-postgresql-using-node-cp2</guid>
      <description>&lt;p&gt;In this post, we’ll create a robust Node.js API for uploading images to Google Cloud Storage while storing associated survey data in PostgreSQL. This setup is ideal for applications requiring managing media uploads and structured metadata. In modern applications, it's common to work with multimedia data, especially images, along with associated metadata that needs to be stored in a database&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: The runtime environment for our application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt;: The relational database for storing survey details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud Storage (GCS)&lt;/strong&gt;: A scalable object storage service for uploading and storing images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Express.js&lt;/strong&gt;: The framework for building our API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multer&lt;/strong&gt;: A middleware for handling &lt;code&gt;multipart/form-data&lt;/code&gt; (file uploads).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow along, you’ll need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Google Cloud Platform (GCP) project with a Cloud Storage bucket.&lt;/li&gt;
&lt;li&gt;PostgreSQL installed and configured with a sample database.&lt;/li&gt;
&lt;li&gt;Basic knowledge of Node.js and SQL.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;This is the file structure of our project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-survey-app/
├── config/
│   ├── dbConfig.js
│   ├── Gcs.js
├── controllers/
│   ├── surveyController.js
├── routes/
│   ├── surveyRoutes.js
├── app.js
├── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Set Up Google Cloud Storage (GCS)
&lt;/h2&gt;

&lt;p&gt;First, configure GCS for image uploads. Here’s how:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a bucket in your GCP console.&lt;/li&gt;
&lt;li&gt;Download the service account JSON key and set the &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; environment variable.
&lt;code&gt;In config/Gcs.js&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require('dotenv').config();
const { Storage } = require('@google-cloud/storage');

const storage = new Storage({
  keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS
});
const bucketName = process.env.GCP_BUCKET_NAME; 
const bucket = storage.bucket(bucketName);

module.exports = { bucket };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Set Up PostgreSQL Database Connection
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;In config/dbConfig.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require('dotenv').config();
const { Pool } = require('pg');

const pool = new Pool({
  user: process.env.DB_USER,
  host: process.env.DB_HOST,
  database: process.env.DB_DATABASE,
  password: process.env.DB_PASSWORD,
  port: process.env.DB_PORT,
});

pool.connect()
  .then(() =&amp;gt; {
    console.log('Database connected successfully');
  })
  .catch((err) =&amp;gt; {
    console.error('Error connecting to the database:', err.message);
  });

module.exports = pool;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Creating the Survey API
&lt;/h2&gt;

&lt;p&gt;Here’s the API that handles survey data and uploads images:&lt;br&gt;
&lt;code&gt;In controllers/surveyController.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const pool = require('../config/dbConfig');
const { bucket } = require('../config/Gcs');
const { PassThrough } = require('stream');

const InsertProjectSurveyDetails = async (req, res) =&amp;gt; {
    const { project_id, user_id, account_id, survey_details } = req.body;
    const imageLists = req.files;

    // Parsing the survey details JSON
    let parsedSurveyDetails;
    try {
        parsedSurveyDetails = JSON.parse(survey_details);
    } catch (error) {
        return res.status(400).json({ Message: 'Invalid JSON format for survey_details' });
    }

    if (!project_id || !user_id || !account_id || !Array.isArray(parsedSurveyDetails) || parsedSurveyDetails.length === 0) {
        return res.status(400).json({ Message: 'Invalid input data' });
    }

    // Validating each survey detail for required fields
    for (const detail of parsedSurveyDetails) {
        const { latitude, longitude, tag, image_id } = detail;
        if (latitude == null || longitude == null || !image_id) {
            return res.status(400).json({ Message: 'Missing latitude, longitude, or image_id in one of the survey details' });
        }
    }

    const insertQuery = `
        INSERT INTO project_survey_details (project_id, user_id, account_id, latitude, longitude, tag, image_name)
        VALUES ($1, $2, $3, $4, $5, $6, $7)
    `;

    const client = await pool.connect();

    try {
        await client.query('BEGIN');

        // Iterate each survey detail and handle image upload and database insertion
        for (const detail of parsedSurveyDetails) {
            const { latitude, longitude, tag, image_id } = detail;

            const imageFile = imageLists.find(img =&amp;gt; img.originalname.includes(image_id));
            if (!imageFile) {
                await client.query('ROLLBACK');
                return res.status(400).json({ Message: 'Image not found for provided image_id' });
            }

            const { originalname: imageName, buffer, stream } = imageFile;
            const imageDestination = bucket.file(`Project-Survey-Images/${imageName}`);
            const imageStream = imageDestination.createWriteStream();

            // Handle Buffer or Stream case for image upload
            const uploadStream = stream ? stream : new PassThrough();
            if (buffer) {
                uploadStream.end(buffer);
            } else if (stream) {
                stream.pipe(uploadStream);
            } else {
                await client.query('ROLLBACK');
                return res.status(400).json({ Message: 'No valid stream or buffer found for image' });
            }
            // Upload images to Bucket and handle error
            await new Promise((resolve, reject) =&amp;gt; {
                imageStream.on('error', (err) =&amp;gt; {
                    reject(err);
                });

                imageStream.on('finish', () =&amp;gt; {
                    resolve();
                });

                uploadStream.pipe(imageStream);
            });

            // Construct image's url 
            const url = `https://storage.googleapis.com/${bucket.name}/Project-Survey-Images/${imageName}`;
            await client.query(insertQuery, [project_id, user_id, account_id, latitude, longitude, tag, imageName]);
        }

        await client.query('COMMIT');
        res.status(201).json({ Message: 'Survey details inserted successfully' });

    } catch (error) {
        await client.query('ROLLBACK');
        console.error('Error Inserting survey details:', error);
        res.status(500).json({ Message: 'Error Inserting survey details' });
    } finally {
        client.release();
    }
};

module.exports = { InsertProjectSurveyDetails };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When dealing with image uploads in Node.js, images can be provided either in &lt;strong&gt;buffer&lt;/strong&gt; form (loaded fully in memory) or stream form (data flows in chunks). Our API handles both formats using the &lt;code&gt;PassThrough&lt;/code&gt; stream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buffer Handling:&lt;/strong&gt; If the image is in buffer format (like in the screenshot below), it is converted into a readable stream using &lt;code&gt;PassThrough&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream Handling:&lt;/strong&gt; If the image is already a stream, it is directly piped through &lt;code&gt;PassThrough&lt;/code&gt;.
This flexibility ensures that the image data is processed consistently, regardless of its original format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqff52ysbsrpi7o2ndpci.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqff52ysbsrpi7o2ndpci.jpeg" alt="image details" width="800" height="96"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Tip: Before implementing this approach, check whether your image contains buffer data or a stream. The API automatically handles both based on the provided content, ensuring robust and efficient processing.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Setting Up Routes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;In routes/surveyRoutes.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const express = require('express');
const multer = require('multer');
const { InsertProjectSurveyDetails } = require('../controllers/surveyController');

const upload = multer(); 
const router = express.Router();

router.post('/survey', upload.array('files'), InsertProjectSurveyDetails);

module.exports = router;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Integrating Everything in &lt;code&gt;app.js&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const express = require('express');
const surveyRoutes = require('./routes/surveyRoutes');

const app = express();

app.use(express.json());
app.use('/api', surveyRoutes);

const PORT = process.env.PORT || 4000;
app.listen(PORT, () =&amp;gt; {
    console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Edge Cases
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validating Survey Data&lt;/strong&gt;: Ensure all required fields like &lt;code&gt;latitude&lt;/code&gt;, &lt;code&gt;longitude&lt;/code&gt;, and &lt;code&gt;image_id&lt;/code&gt; are present in each survey detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Upload Handling&lt;/strong&gt;: Handle cases where the image data might be missing, or the buffer or stream is invalid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Transactions&lt;/strong&gt;: Use PostgreSQL transactions to ensure atomicity. If an error occurs while processing any detail, roll back the transaction.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How the API Works
&lt;/h2&gt;

&lt;p&gt;When sending a request to the API, you provide a list of survey points in a &lt;code&gt;survey_details&lt;/code&gt; array. Each survey point contains the following information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;latitude &amp;amp; longitude&lt;/strong&gt;: The geographical coordinates where the survey was conducted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tag&lt;/strong&gt;: A label that helps you identify the survey point (like "Survey Point A").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;image_id&lt;/strong&gt;: A unique identifier used to find the correct image file.
The image files are also sent along with the request. Each image’s filename contains this &lt;code&gt;image_id&lt;/code&gt;, so we can easily match it with the correct survey point. For example, if the image filename is "66789_survey.jpg", the &lt;code&gt;image_id&lt;/code&gt; for that survey point would be &lt;code&gt;66789&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Use image_id?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;image_id&lt;/code&gt; is important because it ensures each survey point is linked with the correct image. By extracting the &lt;code&gt;image_id&lt;/code&gt; from the image filename, we can precisely match it with the latitude, longitude, and tag you provided. This makes it easier to perform analytics or other operations based on the exact location and the associated images.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to test the API using Postman
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set the request method to &lt;strong&gt;POST&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under the &lt;strong&gt;Body&lt;/strong&gt; tab, select &lt;strong&gt;form-data&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add the following fields:

&lt;ul&gt;
&lt;li&gt;project_id: (text) e.g., &lt;code&gt;4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;user_id: (text) e.g., &lt;code&gt;25&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;account_id: (text) e.g., &lt;code&gt;2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;survey_details: (text) The JSON data is shown below:
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
    {
        "latitude": 37.7749,
        "longitude": -122.4194,
        "tag": "Survey Point A",
        "image_id": "66789"
    },
    {
        "latitude": 37.7750,
        "longitude": -122.4195,
        "tag": "Survey Point B",
        "image_id": "66790"
    }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;strong&gt;Files&lt;/strong&gt;, add the images:&lt;br&gt;
    * &lt;strong&gt;files&lt;/strong&gt;: Upload multiple files where each file’s name includes the &lt;code&gt;image_id&lt;/code&gt; (e.g., &lt;code&gt;66789_survey.jpg&lt;/code&gt;, &lt;code&gt;66790_survey.jpg&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Postman Request and Response Output
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frew70z68cgkq5mknanr4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frew70z68cgkq5mknanr4.jpeg" alt="postman image" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The API actually handles several search points. You can send as many survey points as necessary by adding multiple items to the survey_details structure.&lt;/li&gt;
&lt;li&gt;Each image is uploaded to Google Cloud Storage and the remaining information is stored in the database, making it easier to analyze locations and tags later.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>database</category>
      <category>cloud</category>
      <category>api</category>
    </item>
  </channel>
</rss>
