Efficiently handling image uploads is a critical aspect of backend development, particularly in modern web applications. In this comprehensive guide, we'll demonstrate how to store images using Node.js, TypeScript, PostgreSQL, Multer, and Cloudinary. Whether you're a beginner or looking to enhance your backend skills, this step-by-step tutorial will help you seamlessly implement image uploads.
Prerequisites
Before diving into the implementation, ensure you have the following:
Node.js and npm/yarn installed.- Basic knowledge of TypeScript and Express.js.
- A PostgreSQL database instance.
- A Cloudinary account for image hosting.
1. Initializing the Project
Start by creating a new Node.js project:
npm init -y
Install the required dependencies:
npm install cloudinary dotenv multer pg cors express
npm install --save-dev typescript ts-node eslint nodemon typescript-eslint @eslint/js @types/express @types/multer @types/pg @types/cors
Create a .env file to store your environment variables:
touch .env
Populate the .env file with the following:
PORT=8080
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_HOST=localhost
DB_PORT=5432
DB_NAME=your_db_name
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
2. Configuring TypeScript
Set up TypeScript by creating a tsconfig.json file:
touch tsconfig.json
Add the following configuration:
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"strict": true
},
"lib": ["es2015"]
}
3. Setting Up the Express Server
Create the necessary directories:
mkdir src public
cd public && mkdir temp && cd temp && touch .gitkeep
cd ../src && mkdir controllers db middlewares routes utils
Inside the src directory, create app.ts and index.ts:
src/app.ts
import express from 'express';
import cors from 'cors';
import routes from './routes';
const app = express();
app.use(
cors({
origin: process.env.CORS_ORIGIN,
credentials: true,
})
);
app.use(express.json({ limit: '16kb' }));
app.use(express.urlencoded({ extended: true, limit: '16kb' }));
app.use(express.static('public'));
app.use('/api/v1', routes);
export default app;
src/index.ts
import app from './app';
app.listen(process.env.PORT || 8080, () => {
console.log(`Server running on port ${process.env.PORT}!`);
});
4. Configuring PostgreSQL
Set up database connectivity in src/db/db.ts:
import { Pool } from 'pg';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: Number(process.env.DB_PORT),
});
pool.connect(err => {
if (err) {
console.error('Error connecting to the database:', err);
} else {
console.log('Connected to PostgreSQL database');
}
});
export default pool;
5. Setting Up Multer Middleware
Handle file uploads using Multer by creating src/middlewares/multer.middleware.ts:
import multer from 'multer';
const storage = multer.diskStorage({
destination: function (req, file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) {
cb(null, './public/temp');
},
filename: function (req, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, file.fieldname + '-' + uniqueSuffix);
},
});
const upload = multer({ storage });
export default upload;
6. Integrating Cloudinary
Create src/utils/cloudinary.ts:
import { v2 as cloudinary } from 'cloudinary';
import fs from 'fs';
(async function () {
// Configure Cloudinary
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true,
});
})();
const uploadOnCloudinary = async (file: string) => {
try {
if (!file) return null;
// Upload image to Cloudinary
const result = await cloudinary.uploader.upload(file, {
folder: 'ryde-uber-clone',
resource_type: 'image',
});
// file has been uploaded successfully
console.log('file is uploaded on cloudinary ', result);
// Check if the file exists before attempting to delete it
if (fs.existsSync(file)) {
fs.unlinkSync(file);
} else {
console.warn(`File not found: ${file}`);
}
return result.url;
} catch (error) {
console.error('Error uploading to Cloudinary:', error);
// Check if the file exists before attempting to delete it
if (fs.existsSync(file)) {
fs.unlinkSync(file);
} else {
console.warn(`File not found: ${file}`);
}
return null;
}
};
export default uploadOnCloudinary;
7. Creating Routes and Controllers
Define a route for handling uploads:
src/routes/auth.routes.ts
import express from 'express';
import { signUp } from '../controllers/auth.controllers';
import upload from '../middlewares/multer.middleware';
const router = express.Router();
router.post('/sign-up', upload.single('avatar'), signUp); // User signUp
export default router;
src/routes/index.ts
import express from 'express';
import authRoutes from './auth.routes';
const router = express.Router();
router.use('/auth', authRoutes);
export default router;
src/controllers/auth.controllers.ts
import { Request, Response } from 'express';
import pool from '../db/db';
import asyncHandler from '../utils/asyncHandler';
import uploadOnCloudinary from '../utils/cloudinary';
export const signUp = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const { firstname, lastname, email, password } = req.body;
if (!firstname || !lastname || !email || !password) {
res.status(400).json({ message: 'All fields are required' });
}
// Check if user already exists
const userExists = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (userExists.rows.length > 0) {
res.status(409).json({
success: false,
message: 'User already exists',
});
return;
}
// Upload avatar
const avatarPath = req.file?.path;
let avatar = null;
if (avatarPath) {
avatar = await uploadOnCloudinary(avatarPath);
}
// Make sure you bcrypt the password before saving in Database
const newUser = await pool.query(
'INSERT INTO users (firstname, lastname, email, password, avatar) VALUES ($1, $2, $3, $4, $5) RETURNING id, email, firstname, lastname, avatar',
[firstname, lastname, email, password, avatar]
);
res.status(201).json({
success: true,
message: 'User signed up successfully',
user: newUser.rows[0],
});
});
8. Testing the Application
Start the server:
npm run dev
Use tools like Postman or curl to test the /auth/signup endpoint by sending a POST request with a file and user data.
Resources
You can find the complete code for this project on GitHub:
to treat me to a virtual coffee. Cheers!
SOCIAL SHARE CARD GENERATOR