Build a Simple Next.js Interactive Image Resizer App

Written by

in

In the ever-evolving landscape of web development, optimizing images is crucial for performance and user experience. Large image files can significantly slow down website loading times, leading to frustrated users and potentially lower search engine rankings. While there are numerous tools available, building your own image resizer app in Next.js provides a fantastic opportunity to learn fundamental web development concepts and gain practical skills. This guide will walk you through creating a simple, yet effective, image resizer application, perfect for beginners and intermediate developers alike.

Why Build an Image Resizer App?

Creating an image resizer app offers several benefits:

  • Hands-on Learning: You’ll gain practical experience with essential technologies like Next.js, React, and image manipulation libraries.
  • Performance Optimization: Learn how to reduce image file sizes without sacrificing quality, improving website speed.
  • Customization: Tailor the app to your specific needs, such as adding watermarks or implementing different resizing algorithms.
  • Portfolio Enhancement: Showcase your skills and demonstrate your understanding of web development best practices.

This project is ideal for anyone looking to deepen their understanding of Next.js and frontend development while tackling a real-world problem.

Prerequisites

Before we begin, ensure you have the following installed:

  • Node.js and npm (or yarn): These are essential for managing project dependencies and running the development server.
  • A code editor: (e.g., VS Code, Sublime Text) to write and edit your code.
  • Basic understanding of HTML, CSS, and JavaScript: Familiarity with these technologies will be helpful, but not strictly required.

Setting Up Your Next.js Project

Let’s get started by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app image-resizer-app

This command will create a new Next.js project named “image-resizer-app”. Navigate into the project directory:

cd image-resizer-app

Now, start the development server:

npm run dev

This will start the development server, and you can access your app in your browser at http://localhost:3000.

Installing Dependencies

We’ll need a library to handle image resizing. A popular choice is “sharp”, a high-performance image processing library. Install it using:

npm install sharp

This command adds “sharp” to your project’s dependencies, allowing us to manipulate images.

Project Structure and File Setup

Let’s organize our project. We’ll primarily work within the “pages” directory. Create a file named “resizer.js” inside the “pages” directory. This file will house the core logic of our image resizer app. Your project structure should look something like this:


image-resizer-app/
├── node_modules/
├── pages/
│   └── resizer.js
├── public/
├── .gitignore
├── package.json
├── yarn.lock (if using yarn)
└── ...

Building the Frontend (resizer.js)

Open “pages/resizer.js” and let’s start building the user interface. We’ll use React components for this. Here’s a basic structure:


import { useState } from 'react';

function Resizer() {
  const [image, setImage] = useState(null);
  const [width, setWidth] = useState('');
  const [height, setHeight] = useState('');
  const [resizedImage, setResizedImage] = useState(null);

  const handleImageChange = (e) => {
    // Implement image handling here
  };

  const handleResize = async () => {
    // Implement resizing logic here
  };

  return (
    <div>
      <h2>Image Resizer</h2>
      <input type="file" onChange={handleImageChange} />
      <br />
      <label htmlFor="width">Width:</label>
      <input type="number" id="width" value={width} onChange={(e) => setWidth(e.target.value)} />
      <br />
      <label htmlFor="height">Height:</label>
      <input type="number" id="height" value={height} onChange={(e) => setHeight(e.target.value)} />
      <br />
      <button onClick={handleResize}>Resize</button>
      {resizedImage && (
        <img src={resizedImage} alt="Resized Image" />
      )}
    </div>
  );
}

export default Resizer;

This code sets up the basic UI:

  • State Variables: We use `useState` to manage the image file, width, height, and the resized image.
  • File Input: An `input` element of type “file” allows the user to upload an image.
  • Width and Height Inputs: Input fields for the user to specify the desired width and height.
  • Resize Button: A button that triggers the resizing process.
  • Image Display: An `img` tag to display the resized image.

Handling Image Upload (handleImageChange)

Let’s implement the `handleImageChange` function to handle the image upload:


const handleImageChange = (e) => {
  const file = e.target.files[0];
  if (file) {
    const reader = new FileReader();
    reader.onload = () => {
      setImage(reader.result);
    };
    reader.readAsDataURL(file);
  }
};

This function does the following:

  • Gets the selected file from the input.
  • Uses `FileReader` to read the image file as a data URL.
  • Updates the `image` state with the data URL, which can be used to display the image.

Implementing the Backend (API Route)

Next.js allows us to create API routes within the “pages/api” directory. We’ll create an API route to handle the image resizing process. Create a file named “resize.js” inside the “pages/api” directory. This is where the image processing using “sharp” will happen.


# Create the directory if it doesn't exist
mkdir -p pages/api
# Create the file
touch pages/api/resize.js

Now, let’s add the code to “pages/api/resize.js”:


import sharp from 'sharp';
import fs from 'fs';
import path from 'path';

export const config = {
  api: {
    bodyParser: {
      sizeLimit: '10mb',
    },
  },
};

export default async function handler(req, res) {
  if (req.method === 'POST') {
    try {
      const { image, width, height } = req.body;

      if (!image || !width || !height) {
        return res.status(400).json({ error: 'Missing parameters' });
      }

      // Remove the data URL prefix
      const base64Data = image.replace(/^data:image/jpeg;base64,/, '').replace(/^data:image/png;base64,/, '');

      const imageBuffer = Buffer.from(base64Data, 'base64');

      const parsedWidth = parseInt(width, 10);
      const parsedHeight = parseInt(height, 10);

      const resizedBuffer = await sharp(imageBuffer)
        .resize(parsedWidth, parsedHeight)
        .toBuffer();

      // Convert the resized image buffer to a base64 string
      const resizedBase64 = resizedBuffer.toString('base64');
      const resizedImage = `data:image/jpeg;base64,${resizedBase64}`;

      res.status(200).json({ resizedImage });

    } catch (error) {
      console.error('Resize error:', error);
      res.status(500).json({ error: 'Failed to resize image' });
    }
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

This API route does the following:

  • Imports: Imports necessary modules, including “sharp”, “fs” and “path”.
  • Configures `bodyParser`: Sets the `sizeLimit` to handle larger image files.
  • Handles POST requests: Only processes POST requests.
  • Extracts parameters: Retrieves the image data, width, and height from the request body.
  • Error handling: Checks for missing parameters and returns an error if any are missing.
  • Decodes the image: Removes the data URL prefix and converts the base64 data to a buffer.
  • Resizes the image: Uses “sharp” to resize the image buffer to the specified dimensions.
  • Encodes the image: Converts the resized image buffer back to a base64 string.
  • Returns the resized image: Sends the resized image as a data URL in the response.
  • Error handling: Includes a `try…catch` block to handle any errors during the process.

Implementing the Resize Function (handleResize)

Now, let’s implement the `handleResize` function in “pages/resizer.js” to call the API route:


const handleResize = async () => {
  if (!image || !width || !height) {
    alert('Please upload an image and enter width and height.');
    return;
  }

  try {
    const response = await fetch('/api/resize', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ image, width, height }),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    setResizedImage(data.resizedImage);
  } catch (error) {
    console.error('Resize error:', error);
    alert('Failed to resize image.  Check the console for details.');
  }
};

This function:

  • Input Validation: Checks if an image and dimensions are provided.
  • Fetches the API Route: Uses the `fetch` API to send a POST request to the “/api/resize” route, passing the image data, width, and height.
  • Handles Response: Parses the JSON response from the API route and updates the `resizedImage` state with the resized image data URL.
  • Error Handling: Includes a `try…catch` block to handle potential errors during the API call.

Complete Code for resizer.js

Here’s the complete code for “pages/resizer.js”:


import { useState } from 'react';

function Resizer() {
  const [image, setImage] = useState(null);
  const [width, setWidth] = useState('');
  const [height, setHeight] = useState('');
  const [resizedImage, setResizedImage] = useState(null);

  const handleImageChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = () => {
        setImage(reader.result);
      };
      reader.readAsDataURL(file);
    }
  };

  const handleResize = async () => {
    if (!image || !width || !height) {
      alert('Please upload an image and enter width and height.');
      return;
    }

    try {
      const response = await fetch('/api/resize', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ image, width, height }),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      setResizedImage(data.resizedImage);
    } catch (error) {
      console.error('Resize error:', error);
      alert('Failed to resize image.  Check the console for details.');
    }
  };

  return (
    <div>
      <h2>Image Resizer</h2>
      <input type="file" onChange={handleImageChange} />
      <br />
      <label htmlFor="width">Width:</label>
      <input type="number" id="width" value={width} onChange={(e) => setWidth(e.target.value)} />
      <br />
      <label htmlFor="height">Height:</label>
      <input type="number" id="height" value={height} onChange={(e) => setHeight(e.target.value)} />
      <br />
      <button onClick={handleResize}>Resize</button>
      {resizedImage && (
        <img src={resizedImage} alt="Resized Image" />
      )}
    </div>
  );
}

export default Resizer;

Running and Testing the App

Save both files (“pages/resizer.js” and “pages/api/resize.js”). Start or restart your Next.js development server (`npm run dev`). Navigate to http://localhost:3000/resizer (or the appropriate port if you’re using a different one). You should see the image resizer interface.

Testing Steps:

  1. Upload an image using the “Choose File” button.
  2. Enter the desired width and height.
  3. Click the “Resize” button.
  4. The resized image should appear below the button.

If everything works correctly, congratulations! You’ve built a basic image resizer app.

Common Mistakes and Troubleshooting

Here are some common issues and how to resolve them:

  • CORS Errors: If you encounter CORS (Cross-Origin Resource Sharing) errors, ensure your API route is correctly configured to handle requests from your frontend. In a basic setup, this shouldn’t be an issue, but if you deploy to a different domain, you may need to configure CORS.
  • Image Not Displaying: Double-check the `src` attribute of the `img` tag. Make sure it’s correctly bound to the `resizedImage` state variable. Also, verify that the data URL is being generated correctly by inspecting the browser’s developer tools (Network tab) to see the response from the API route.
  • Sharp Installation Issues: Ensure “sharp” is installed correctly and that there are no errors in the console related to its import or use. Sometimes, specific system dependencies might be required for “sharp”. Consult the “sharp” documentation for troubleshooting installation issues.
  • Server-Side Errors: Check the console logs in your terminal and browser’s developer tools for any errors from the API route. This will help you pinpoint the cause of the problem (e.g., incorrect parameters, image processing errors).
  • Incorrect Image Format: The code provided assumes the output is a JPEG. If you want to support other formats (like PNG), modify the `resizedImage` string to reflect the desired format (e.g., `data:image/png;base64,…`).

Enhancements and Next Steps

This is a basic implementation. You can extend this project in numerous ways:

  • Add image format selection: Allow users to choose the output image format (JPEG, PNG, WebP).
  • Implement quality settings: Allow users to control the image quality.
  • Add error handling: Provide more user-friendly error messages and handle different types of image processing errors gracefully.
  • Implement progress indicators: Show a loading indicator while the image is being resized.
  • Add a drag-and-drop feature: Allow users to drag and drop images onto the page.
  • Optimize for performance: Implement caching and other techniques to improve the performance of the resizing process.
  • Implement different resizing algorithms: Explore different resizing algorithms offered by “sharp” (e.g., `cover`, `contain`, `inside`, `outside`).
  • Add a preview: Display a preview of the resized image before the user downloads it.

Key Takeaways

Building an image resizer app in Next.js is a practical exercise that combines frontend and backend development. You’ve learned how to handle file uploads, process images using “sharp”, create API routes, and manage state in a React application. This project provides a solid foundation for understanding image optimization and web development principles. By experimenting with different features and enhancements, you can further expand your skills and create a more sophisticated and user-friendly application. This project not only equips you with valuable technical skills but also fosters a deeper appreciation for the importance of performance optimization in web development.