Last updated: February 5, 2025

<span aria-hidden="true" id="creating-a-simple-image-processing-api-with-python-and-flask"></span>

# Creating a simple image processing API with Python and Flask

![Kevin van Zonneveld](/assets/images/teammates/avatar-kvz-4.jpg?dpl=dpl_AsMTCxwVXNuJ5TmdPevSCe63JuJo)

**Kevin van Zonneveld**

Co-founder · Amsterdam, The Netherlands · Show bio

[](https://x.com/kvz)[](https://github.com/kvz)

Image processing APIs enable developers to programmatically manipulate and transform images. In this tutorial, we'll build a straightforward yet functional image processing API using Python and Flask, demonstrating how to handle common operations like resizing and format conversion. This demo also showcases robust error handling, rate limiting, and secure file validation practices.

<span aria-hidden="true" id="introduction-to-image-processing-apis"></span>

## Introduction to image processing APIs

An image processing API provides endpoints that accept image files as input, perform specified operations, and return the processed images. These APIs are essential for applications that need to handle user-uploaded images, generate thumbnails, or convert between different image formats.

<span aria-hidden="true" id="setting-up-the-development-environment"></span>

## Setting up the development environment

Begin by setting up your development environment:

1. Create a new directory for your project and a requirements.txt file:

```bash
mkdir flask-image-api  
cd flask-image-api  
touch requirements.txt  
```

2. Add the following dependencies to requirements.txt:

```pip-requirements
Flask==3.1.0  
Pillow==11.1.0  
gunicorn==23.0.0  
flask-cors==4.0.0  
flask-limiter==3.5.0  
```

3. Set up a virtual environment and install dependencies:

```bash
python3 -m venv venv  
source venv/bin/activate  # On Windows use `venv\Scripts\activate`  
pip install -r requirements.txt  
```

<span aria-hidden="true" id="creating-a-basic-flask-application"></span>

## Creating a basic Flask application

Create a file named `app.py` with the following content:

```python
from flask import Flask, request, send_file
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from PIL import Image
import io

app = Flask(__name__)
CORS(app)
limiter = Limiter(app, key_func=get_remote_address)

# Configure maximum file size (16 mb)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024

# Allowed file extensions
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    return 'Welcome to the Image Processing API!'

if __name__ == '__main__':
    app.run(debug=True)

```

<span aria-hidden="true" id="integrating-pillow-for-image-processing"></span>

## Integrating pillow for image processing

Create a helper function to load and validate images. This function checks both the file extension and its MIME type, ensuring only valid image files are processed:

```python
def load_image(file):
    try:
        if not allowed_file(file.filename) or not file.mimetype.startswith('image/'):
            return None
        img = Image.open(file)
        img.verify()  # Verify image integrity
        file.seek(0)  # Reset file pointer after verification
        return Image.open(file)
    except Exception:
        return None

```

<span aria-hidden="true" id="implementing-image-resizing-endpoint"></span>

## Implementing image resizing endpoint

Add an endpoint for resizing images with appropriate error handling and validation:

```python
@app.route('/resize', methods=['POST'])
@limiter.limit('100 per day')
def resize_image():
    try:
        if 'file' not in request.files:
            return {'error': 'No file part'}, 400

        file = request.files['file']
        if file.filename == '':
            return {'error': 'No selected file'}, 400

        img = load_image(file)
        if img is None:
            return {'error': 'Invalid image file'}, 400

        width = int(request.form.get('width', 100))
        height = int(request.form.get('height', 100))

        if width <= 0 or height <= 0:
            return {'error': 'Invalid dimensions'}, 400

        resized_img = img.resize((width, height))

        img_io = io.BytesIO()
        resized_img.save(img_io, format=img.format or 'JPEG')
        img_io.seek(0)

        return send_file(
            img_io,
            mimetype=f'image/{img.format.lower() if img.format else "jpeg"}'
        )

    except Exception as e:
        return {'error': str(e)}, 500

```

<span aria-hidden="true" id="adding-image-format-conversion-endpoint"></span>

## Adding image format conversion endpoint

Include another endpoint to handle image format conversion. This endpoint accepts a target format, converts the image accordingly, and handles any potential errors:

```python
@app.route('/convert', methods=['POST'])
@limiter.limit('100 per day')
def convert_image():
    try:
        if 'file' not in request.files:
            return {'error': 'No file part'}, 400

        file = request.files['file']
        if file.filename == '':
            return {'error': 'No selected file'}, 400

        img = load_image(file)
        if img is None:
            return {'error': 'Invalid image file'}, 400

        target_format = request.form.get('format', 'PNG').upper()
        if target_format not in ['PNG', 'JPEG', 'GIF']:
            return {'error': 'Unsupported format'}, 400

        img_io = io.BytesIO()
        if target_format == 'JPEG':
            img = img.convert('RGB')  # Remove alpha channel for JPEG compatibility
        img.save(img_io, format=target_format)
        img_io.seek(0)

        return send_file(img_io, mimetype=f'image/{target_format.lower()}')

    except Exception as e:
        return {'error': str(e)}, 500

```

<span aria-hidden="true" id="testing-the-api-with-sample-requests"></span>

## Testing the API with sample requests

Use cURL to test the endpoints. The following examples demonstrate how to call the resizing and conversion endpoints:

```bash
# Resize an image
curl -fsSL -X POST -F "file=@path/to/image.jpg" -F "width=300" -F "height=200" \
  http://localhost:5000/resize -o resized_image.jpg

# Convert an image to PNG
curl -fsSL -X POST -F "file=@path/to/image.jpg" -F "format=PNG" \
  http://localhost:5000/convert -o converted_image.png

```

<span aria-hidden="true" id="error-handling-and-input-validation"></span>

## Error handling and input validation

For production, comprehensive testing is essential. Create a file named `test_app.py` with the following tests to validate the API's functionality:

```python
import pytest
from app import app
import io
from PIL import Image

@pytest.fixture
def client():
    app.config['TESTING'] = True
    with app.test_client() as client:
        yield client


def test_resize_endpoint(client):
    # Create a test image in memory
    img_io = io.BytesIO()
    Image.new('RGB', (100, 100)).save(img_io, 'JPEG')
    img_io.seek(0)

    data = {
        'file': (img_io, 'test.jpg'),
        'width': '50',
        'height': '50'
    }
    response = client.post('/resize', data=data)
    assert response.status_code == 200


def test_invalid_file(client):
    data = {
        'file': (io.BytesIO(b'invalid'), 'test.txt')
    }
    response = client.post('/resize', data=data)
    assert response.status_code == 400

```

<span aria-hidden="true" id="deploying-the-api-to-a-server"></span>

## Deploying the API to a server

For production deployment, use Gunicorn along with proper configuration. Create a Gunicorn configuration file named `gunicorn_config.py`:

```python
import multiprocessing

workers = multiprocessing.cpu_count() * 2 + 1
bind = '127.0.0.1:8000'
keepalive = 120
errorlog = '/var/log/gunicorn/error.log'
accesslog = '/var/log/gunicorn/access.log'
worker_class = 'sync'
max_requests = 1000
max_requests_jitter = 50
timeout = 120

```

Then, create a systemd service file at `/etc/systemd/system/flask-image-api.service`:

```ini
[Unit]
Description=Flask Image Processing API
After=network.target

[Service]
User=www-data
WorkingDirectory=/path/to/flask-image-api
Environment="PATH=/path/to/flask-image-api/venv/bin"
ExecStart=/path/to/flask-image-api/venv/bin/gunicorn -c gunicorn_config.py app:app

[Install]
WantedBy=multi-user.target

```

Enable and start the service:

```bash
sudo systemctl enable flask-image-api
sudo systemctl start flask-image-api

```

<span aria-hidden="true" id="conclusion-and-next-steps"></span>

## Conclusion and next steps

We have built a secure, efficient, and scalable image processing API using Python, Flask, and Pillow. The API incorporates essential features like file validation, error handling, rate limiting, and CORS support. To extend this solution, you might consider options such as image compression, user authentication, batch processing, cloud storage integration, or metadata extraction.

For a comprehensive solution with advanced file processing capabilities, consider exploring[Transloadit](/index.md), which offers a robust platform for handling and transforming files in your applications.

\#python#flask#image-processing#api-development#handling-uploads-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed

Cancel anytime
