Creating an OCR API with FaaS on Azure - Part 2: Persisting Data in Azure Postgres SQL with Best Practices

In the first part of this series, we built a Function App that securely receives an image via HTTP and saves it to Azure Blob Storage, using managed identity. Now, let's move forward and record metadata of these images in a database. Spoiler alert: we'll use Azure Postgres SQL, a layered architecture, and best practices like SOLID principles and responsibility segregation. Why save data in a database? Blob Storage is excellent for storing files, but what if you want to know: When a file was uploaded? What is the public URL of the image? What is the OCR processing status (pending, processed, error)? Which images are medical prescriptions, for instance? That's where Azure Postgres SQL steps in, to maintain control of everything going on with each image. Updating Project Architecture An IImageRepository.ts file has been added to manage the database domain logic and its implementation within OcrImageRepository.ts: /ocr-function-app ├── application/ │ └── UploadImageService.ts ├── domain/ │ └── IImageStorage.ts │ └── IImageRepository.ts ├── infrastructure/ │ └── AzureBlobStorage.ts │ └── OcrImageRepository.ts ├── validations/ │ └── ContentTypeValidator.ts ├── HttpAddToBlob/ │ └── index.ts │ └── function.json ├── constants.ts ├── host.json ├── local.settings.json └── package.json The idea is to keep a lightweight DDD approach, delegating responsibilities to more specific layers. Validating Content Type Before processing anything arriving at the Function, let's validate if the content is a real image. First, we create an enum to store the allowed types: export enum AllowedContentTypes { JPEG = 'image/jpeg', PNG = 'image/png', JPG = 'image/jpg', } Then, we craft a simple validation class: import { AllowedContentTypes } from "../constants"; export class ContentTypeValidator { private static allowedTypes = Object.values(AllowedContentTypes); static validate(contentType?: AllowedContentTypes): void { if (!contentType || !this.allowedTypes.includes(contentType)) { throw new Error('Unsupported content type. Send a JPEG or PNG image.'); } } } Inside the Function: const contentType = req.headers['content-type']; ContentTypeValidator.validate(contentType); Creating the Database Table Once the database is set up, execute this script via Azure Data Studio or SSMS to create the table: CREATE TABLE OcrImages ( Id INT IDENTITY PRIMARY KEY, FileName NVARCHAR(200) NOT NULL, Url NVARCHAR(MAX) NOT NULL, UploadDate DATETIME NOT NULL DEFAULT GETDATE(), Status NVARCHAR(50) NOT NULL DEFAULT 'pending', IsPrescription BOOLEAN NOT NULL DEFAULT false ); Packages Used Install the following packages for your TypeScript project with Azure Functions: npm install pg Purpose: Connect to Azure Postgres SQL (pg). Connecting the Function to Azure Postgres SQL Avoid hardcoding connection strings with usernames and passwords by using Microsoft Entra ID (managed identity) via the Service Connector feature in the Azure portal. Steps to connect: Access your Function App in the Azure portal. Click Service Connector > + Add. Select Azure Postgres SQL as the destination. Choose User Assigned. Set up Firewall settings. Click Next: Review + Create and Create. Defining Repository Contracts Create the IImageRepository interface to define persistence capabilities, independent of the database type: export interface IImageRepository { save(fileName: string, url: string): Promise; } Implementing with Azure Postgres SQL Now, for the actual implementation under infrastructure: import { IImageRepository } from "../domain/IImageRepository"; import { Pool } from "pg"; export class OcrImageRepository implements IImageRepository { constructor(private readonly pool: Pool) {} async save(fileName: string, url: string): Promise { try { await this.pool.query( ` INSERT INTO OcrImages (FileName, Url) VALUES ($1, $2) `, [fileName, url] ); } catch (err) { throw new Error(`Error inserting image: ${(err as Error).message}`); } } } Notice how this class only manages connections and inserts, knowing nothing about HTTP, OCR, or validations—true to SOLID principles. Update the application service with dependencies: export class UploadImageService { constructor( private readonly imageStorage: IImageStorage, private readonly imageRepository: IImageRepository, ) {} async handleUpload(buffer: Buffer): Promise { const fileName = `${uuidv4()}.pngg`; const url = await this.imageStorage.uploadImage(buffer, fileName); await this.imageRepository.save(fileName, url); return { url, fileName }; } } Current Function Structure The HTTP Function now: Validates content type (Content-Type). Checks image size. Saves the image to Azure Blob Storage. Records image name and URL i

Apr 17, 2025 - 14:26
 0
Creating an OCR API with FaaS on Azure - Part 2: Persisting Data in Azure Postgres SQL with Best Practices

In the first part of this series, we built a Function App that securely receives an image via HTTP and saves it to Azure Blob Storage, using managed identity. Now, let's move forward and record metadata of these images in a database. Spoiler alert: we'll use Azure Postgres SQL, a layered architecture, and best practices like SOLID principles and responsibility segregation.

Why save data in a database?

Blob Storage is excellent for storing files, but what if you want to know:

  • When a file was uploaded?
  • What is the public URL of the image?
  • What is the OCR processing status (pending, processed, error)?
  • Which images are medical prescriptions, for instance?

That's where Azure Postgres SQL steps in, to maintain control of everything going on with each image.

Updating Project Architecture

An IImageRepository.ts file has been added to manage the database domain logic and its implementation within OcrImageRepository.ts:

/ocr-function-app
├── application/
│   └── UploadImageService.ts
├── domain/
│   └── IImageStorage.ts
│   └── IImageRepository.ts
├── infrastructure/
│   └── AzureBlobStorage.ts
│   └── OcrImageRepository.ts
├── validations/
│   └── ContentTypeValidator.ts
├── HttpAddToBlob/
│   └── index.ts
│   └── function.json
├── constants.ts
├── host.json
├── local.settings.json
└── package.json

The idea is to keep a lightweight DDD approach, delegating responsibilities to more specific layers.

Validating Content Type

Before processing anything arriving at the Function, let's validate if the content is a real image.

First, we create an enum to store the allowed types:

export enum AllowedContentTypes {
    JPEG = 'image/jpeg',
    PNG = 'image/png',
    JPG = 'image/jpg',
}

Then, we craft a simple validation class:

import { AllowedContentTypes } from "../constants";

export class ContentTypeValidator {
    private static allowedTypes = Object.values(AllowedContentTypes);

    static validate(contentType?: AllowedContentTypes): void {
        if (!contentType || !this.allowedTypes.includes(contentType)) {
            throw new Error('Unsupported content type. Send a JPEG or PNG image.');
        }
    }
}

Inside the Function:

const contentType = req.headers['content-type'];
ContentTypeValidator.validate(contentType);

Creating the Database Table

Once the database is set up, execute this script via Azure Data Studio or SSMS to create the table:

CREATE TABLE OcrImages (
    Id INT IDENTITY PRIMARY KEY,
    FileName NVARCHAR(200) NOT NULL,
    Url NVARCHAR(MAX) NOT NULL,
    UploadDate DATETIME NOT NULL DEFAULT GETDATE(),
    Status NVARCHAR(50) NOT NULL DEFAULT 'pending',
    IsPrescription BOOLEAN NOT NULL DEFAULT false
);

Packages Used

Install the following packages for your TypeScript project with Azure Functions:

npm install pg

Purpose:

  • Connect to Azure Postgres SQL (pg).

Connecting the Function to Azure Postgres SQL

Avoid hardcoding connection strings with usernames and passwords by using Microsoft Entra ID (managed identity) via the Service Connector feature in the Azure portal.

Steps to connect:

  1. Access your Function App in the Azure portal.
  2. Click Service Connector > + Add.
  3. Select Azure Postgres SQL as the destination.
  4. Choose User Assigned.
  5. Set up Firewall settings.
  6. Click Next: Review + Create and Create.

Defining Repository Contracts

Create the IImageRepository interface to define persistence capabilities, independent of the database type:

export interface IImageRepository {
  save(fileName: string, url: string): Promise<void>;
}

Implementing with Azure Postgres SQL

Now, for the actual implementation under infrastructure:

import { IImageRepository } from "../domain/IImageRepository";
import { Pool } from "pg";

export class OcrImageRepository implements IImageRepository {
  constructor(private readonly pool: Pool) {}

  async save(fileName: string, url: string): Promise<void> {
    try {
      await this.pool.query(
        `
        INSERT INTO OcrImages (FileName, Url)
        VALUES ($1, $2)
        `,
        [fileName, url]
      );
    } catch (err) {
      throw new Error(`Error inserting image: ${(err as Error).message}`);
    }
  }
}

Notice how this class only manages connections and inserts, knowing nothing about HTTP, OCR, or validations—true to SOLID principles.

Update the application service with dependencies:

export class UploadImageService {
  constructor(
    private readonly imageStorage: IImageStorage,
    private readonly imageRepository: IImageRepository,
  ) {}

  async handleUpload(buffer: Buffer): Promise<{ url: string; fileName: string }> {
    const fileName = `${uuidv4()}.pngg`;
    const url = await this.imageStorage.uploadImage(buffer, fileName);
    await this.imageRepository.save(fileName, url);
    return { url, fileName };
  }
}

Current Function Structure

The HTTP Function now:

  • Validates content type (Content-Type).
  • Checks image size.
  • Saves the image to Azure Blob Storage.
  • Records image name and URL in Azure Postgres SQL.

Here's the complete Function code:

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
  try {
    if (!req.body) {
      context.res = {
        status: 400,
        body: "Invalid or missing image"
      };
      return;
    }

    const contentType = req.headers['content-type'];
    ContentTypeValidator.validate(contentType as AllowedContentTypes);

    const buffer = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body);

    if (buffer.length > 15 * 1024 * 1024) {
      throw new Error("Image exceeds maximum size of 15MB.");
    }

    const credential = new DefaultAzureCredential({ managedIdentityClientId });
    const storage = new AzureBlobStorage(accountUrl, containerName, credential);

    const { token: password } = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default');

    const pool = new Pool({
      host, user, password, database, port, ssl,
    });

    const repository = new OcrImageRepository(pool);

    const uploadService = new UploadImageService(storage, repository);
    const { url, fileName } = await uploadService.handleUpload(buffer);

    context.res = {
      status: 200,
      body: {
        message: "Image stored successfully",
        url,
        fileName,
      },
    };
  } catch (error) {
    context.log.error("Error storing image", error);
    context.res = {
      status: 500,
      body: "Error storing image",
      error,
    };
  }
};

export default httpTrigger;