I have this getProducts in my backend:
export const getProducts = async (req: Request, res: Response) => {
try {
const search = (req.query.search as string || '').toLowerCase();
const categoryId = req.query.c as string;
const subcategoryId = req.query.sc as string;
const brandId = req.query.b as string;

    const conditions = [];
    const queryParams: any[] = [];

    if (search) {
        const searchWords = search.split(/\s+/).filter(Boolean);
        const searchConditions = searchWords.map((word) => {
            queryParams.push(`${word}%`, `%${word}%`);
            return `(LOWER(p.name) LIKE $${queryParams.length - 1} OR LOWER(p.name) LIKE $${queryParams.length})`;
        });
        conditions.push(`(${searchConditions.join(' OR ')})`);
    }

    if (categoryId) {
        queryParams.push(categoryId);
        conditions.push(`(c.id = $${queryParams.length} OR EXISTS (
                SELECT 1 FROM subcategories sub 
                WHERE sub.category_id = $${queryParams.length} 
            AND (sub.id = sc.id OR sub.id = p.brand_id)
        ))`);
    }

    if (subcategoryId) {
        queryParams.push(subcategoryId);
        conditions.push(`(sc.id = $${queryParams.length} OR p.brand_id = $${queryParams.length})`);
    }

    if (brandId) {
        queryParams.push(brandId);
        conditions.push(`p.brand_id = $${queryParams.length}`);
        }

        const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

        // Add search to queryParams
        queryParams.push(search);

        const productQuery = `
            WITH filtered_products AS (
                SELECT p.*, 
                       sc.id AS subcategory_id, sc.name AS subcategory_name,
                       c.id AS main_category_id, c.name AS main_category_name
                FROM products p
                LEFT JOIN product_categories pc ON p.id = pc.product_id
                LEFT JOIN subcategories sc ON pc.category_id = sc.id
                LEFT JOIN categories c ON sc.category_id = c.id
                ${whereClause}
                ORDER BY 
                    CASE 
                        WHEN $${queryParams.length} != '' THEN 
                        CASE 
                            WHEN LOWER(p.name) LIKE LOWER($${queryParams.length} || '%') THEN 1
                                WHEN LOWER(p.name) LIKE '%' || LOWER($${queryParams.length}) || '%' THEN 2
                            ELSE 3
                        END
                    ELSE 0
                END,
                p.created_at DESC
        )
        SELECT 
            fp.*,
            (SELECT jsonb_agg(jsonb_build_object(
                'id', c.id, 'name', c.name, 'price', c.price, 'qty', c.qty, 'sold', c.sold,
                'images', (SELECT jsonb_agg(i.url) FROM images i WHERE i.color_id = c.id),
                'sizes', (SELECT jsonb_agg(jsonb_build_object(
                    'id', s.id, 'name', s.name, 'price', s.price, 'qty', s.qty, 'sold', s.sold,
                    'images', (SELECT jsonb_agg(i.url) FROM images i WHERE i.size_id = s.id)
                )) FROM sizes s WHERE s.color_id = c.id)
            )) FROM colors c WHERE c.product_id = fp.id) AS colors,
            (SELECT jsonb_agg(jsonb_build_object(
                'id', s.id, 'name', s.name, 'price', s.price, 'qty', s.qty, 'sold', s.sold, 'color_id', s.color_id,
                'images', (SELECT jsonb_agg(i.url) FROM images i WHERE i.size_id = s.id)
            )) FROM sizes s WHERE s.product_id = fp.id AND s.color_id IS NULL) AS sizes,
            (SELECT jsonb_agg(i.url) FROM images i WHERE i.product_id = fp.id AND i.color_id IS NULL AND i.size_id IS NULL) AS images
        FROM filtered_products fp
    `;

    const productResult = await query(productQuery, queryParams);

    const products = productResult.rows.map(product => ({
        id: product.id,
        name: product.name,
        description: product.description,
        brand_id: product.brand_id,
        has_color: product.has_color,
        has_size: product.has_size,
        product_type: product.product_type,
        price: product.price,
        qty: product.qty,
        sold: product.sold,
        created_at: product.created_at,
        updated_at: product.updated_at,
        categories: [
            {
                id: product.subcategory_id,
                name: product.subcategory_name,
                main_category_id: product.main_category_id,
                main_category_name: product.main_category_name
            }
        ],
        colors: product.colors || [],
        sizes: product.sizes || [],
        images: [
            ...(product.images || []),
            ...(product.colors || []).flatMap((c: { images: any; }) => c.images || []),
            ...(product.sizes || []).flatMap((s: { images: any; }) => s.images || [])
        ]
    }));

    const result = {
        products,
        total: products.length
    };

    res.json(result);
} catch (error) {
    console.error('Error fetching products:', error);
    res.status(500).json({ error: 'Internal server error' });
}

};

and I have this in products page:
import React from 'react';
import ProductList from '@/components/ProductList';
import AsyncNavBar from '@/components/AsyncNavBar';
import { unstable_cache } from 'next/cache';
import Link from 'next/link';

const ITEMS_PER_PAGE = 15;
export const revalidate = 3600

// Wrap fetchSubBrands with unstable_cache
const fetchSubBrands = unstable_cache(
async () => {
const response = await fetch('https://api.naayiq.com/subcategories/brands', {
headers: { 'Content-Type': 'application/json' },
cache: "force-cache",
next: {revalidate: 3600}
});

    if (response.status === 200) {
        return response.json();
    }
    throw new Error(`Failed to fetch sub-brands: ${response.statusText}`);
},
{
    revalidate: 1440,
}

);

const fetchProducts = async () => {
const response = await fetch('https://api.naayiq.com/products', {
headers: {'Content-Type': 'application/json'},
next: {revalidate: 3600}
});

if (!response.ok) {
    throw new Error('Failed to fetch products');
}
return response.json();

}

const getFilteredProducts = async (
page = 1,
query = '',
c = '',
sc = '',
b = '',
minPriceA = 0,
maxPriceA = Infinity,
availability = 'all',
sortBy = ''
) => {
const [productsData, subBrands] = await Promise.all([
fetchProducts(),
b ? fetchSubBrands() : Promise.resolve([]), // Uses unstable_cache: fetched once every 4 hours if 'b' is truthy
]);

let products = productsData.products;

const subCategories = b
    ? subBrands
        .filter((item) => item.category_id.toString() === b)
        .map((item) => item.id.toString())
    : [];

// Calculate min and max prices
let minPrice = Infinity;
let maxPrice = -Infinity;
products.forEach((product) => {
    product.sizes.forEach((size) => {
        const price = parseFloat(size.price);
        if (price < minPrice) minPrice = price;
        if (price > maxPrice) maxPrice = price;
    });
});

const filterProduct = (product) => {
    if (
        c &&
        !product.categories.some((cat) => cat.main_category_id?.toString() === c)
    )
        return false;
    if (
        sc &&
        product?.brand_id.toString() !== sc &&
        !product.categories.some((cat) => cat.id?.toString() === sc)
    )
        return false;
    if (b && !subCategories.includes(product?.brand_id.toString())) return false;

    // Price range filter
    const productPrices = product.sizes.map((size) => parseFloat(size.price));
    const productMinPrice = Math.min(...productPrices);
    const productMaxPrice = Math.max(...productPrices);

    if (productMinPrice < minPriceA || productMaxPrice > maxPriceA) {
        return false;
    }

    // Availability filter
    if (availability !== 'all') {
        const inStock = product.sizes.some((size) => size.qty > 0);
        if (
            (availability === 'in_stock' && !inStock) ||
            (availability === 'out_of_stock' && inStock)
        ) {
            return false;
        }
    }

    // Query filter
    if (query) {
        const normalizedName = product.name.normalize('NFC').toLowerCase();
        const cleanName = normalizedName.replace(
            /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g,
            ''
        );
        const queryWords = query.toLowerCase().split(/\s+/);
        return queryWords.every((word) => cleanName.includes(word));
    }

    return true;
};

// Apply the filter
let filteredProducts = products.filter(filterProduct);

// Calculate total sold for each product
filteredProducts.forEach((product) => {
    product.totalSold = product.sizes.reduce((sum, size) => sum + size.sold, 0);
});

// Apply sorting
switch (sortBy) {
    case 'Price: Low to High':
        filteredProducts.sort(
            (a, b) =>
                Math.min(...a.sizes.map((s) => parseFloat(s.price))) -
                Math.min(...b.sizes.map((s) => parseFloat(s.price)))
        );
        break;
    case 'Price: High to Low':
        filteredProducts.sort(
            (a, b) =>
                Math.max(...b.sizes.map((s) => parseFloat(s.price))) -
                Math.max(...a.sizes.map((s) => parseFloat(s.price)))
        );
        break;
    case 'Name: A to Z':
        filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
        break;
    case 'Name: Z to A':
        filteredProducts.sort((a, b) => b.name.localeCompare(a.name));
        break;
    case 'Best Selling':
        filteredProducts.sort((a, b) => b.totalSold - a.totalSold);
        break;
    case 'Newest Arrivals':
        filteredProducts.sort(
            (a, b) => new Date(b.created_at) - new Date(a.created_at)
        );
        break;
    default:
        // Default sorting (optional)
        break;
}

// Additional query-based sorting if applicable
if (query && !sortBy) {
    const lowerQuery = query.toLowerCase();
    filteredProducts.sort((a, b) => {
        const aIndex = a.name.toLowerCase().indexOf(lowerQuery);
        const bIndex = b.name.toLowerCase().indexOf(lowerQuery);
        return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
    });
}

const totalProducts = filteredProducts.length;
const end = page * ITEMS_PER_PAGE;

return {
    products: filteredProducts.slice(0, end),
    totalProducts,
    hasMore: end < totalProducts,
    minPrice,
    maxPrice,
};

};

// The main ProductsPage component
const ProductsPage = async ({ searchParams }) => {
// Destructure and parse search parameters
const {
page = '1',
query = '',
c = '',
sc = '',
b = '',
minPrice = '0',
maxPrice = '',
availability = 'all',
sortBy = '',
title = '',
} = searchParams;

const pageNum = parseInt(page, 10) || 1;
const minPriceNum = parseFloat(minPrice) || 0;
const maxPriceNum = maxPrice ? parseFloat(maxPrice) : Infinity;

try {
    // Get filtered products based on search parameters
    const { products, totalProducts, hasMore, minPrice, maxPrice } = await getFilteredProducts(
        pageNum,
        query,
        c,
        sc,
        b,
        minPriceNum,
        maxPriceNum,
        availability,
        sortBy
    );

    return (
        <>
            <AsyncNavBar />
            <ProductList
                initialProducts={products}
                totalProducts={totalProducts}
                hasMore={hasMore}
                initialPage={pageNum}
                initialQuery={query}
                initialC={c}
                initialSc={sc}
                initialB={b}
                minPrice={minPrice}
                maxPrice={maxPrice}
                title={sortBy || title || 'All Products'}
            />
        </>
    );
} catch (error) {
    // Enhanced error handling  user-friendly message
    console.error('Error fetching products:', error);
    return (
        <div className="font-serif flex flex-col justify-center items-center p-4">
            <p>Oops! Something went wrong while loading products. Please try again later.</p>
            <Link href="/" className="text-blue-500 underline mt-2">
                Go back home
            </Link>
        </div>
    );
}

};

export default ProductsPage;
However I decided to move to client side rendering instead of server side rendering so I want you to keep getProducts get all the products first... and then do the filter and search logic as server side rendering... so what I mean is move the logic from server side rendering to the backend entierly...
I also moved from serverless to ubuntu 1gb vps.
Do all the required changes.. also don't forgot to optimize for vps...
I'm using express 5.0.
Docs:
Changes in Express 5

Removed methods and properties

    app.del()
    app.param(fn)
    Pluralized method names
    Leading colon in name argument to app.param(name, fn)
    req.param(name)
    res.json(obj, status)
    res.jsonp(obj, status)
    res.send(body, status)
    res.send(status)
    res.sendfile()

Changed

    Path route matching syntax
    Rejected promises handled from middleware and handlers
    app.router
    req.host
    req.query

Improvements

    res.render()

Removed methods and properties

If you use any of these methods or properties in your app, it will crash. So, you’ll need to change your app after you update to version 5.
app.del()

Express 5 no longer supports the app.del() function. If you use this function, an error is thrown. For registering HTTP DELETE routes, use the app.delete() function instead.

Initially, del was used instead of delete, because delete is a reserved keyword in JavaScript. However, as of ECMAScript 6, delete and other reserved keywords can legally be used as property names.
app.param(fn)

The app.param(fn) signature was used for modifying the behavior of the app.param(name, fn) function. It has been deprecated since v4.11.0, and Express 5 no longer supports it at all.
Pluralized method names

The following method names have been pluralized. In Express 4, using the old methods resulted in a deprecation warning. Express 5 no longer supports them at all:

req.acceptsCharset() is replaced by req.acceptsCharsets().

req.acceptsEncoding() is replaced by req.acceptsEncodings().

req.acceptsLanguage() is replaced by req.acceptsLanguages().
Leading colon (:) in the name for app.param(name, fn)

A leading colon character (:) in the name for the app.param(name, fn) function is a remnant of Express 3, and for the sake of backwards compatibility, Express 4 supported it with a deprecation notice. Express 5 will silently ignore it and use the name parameter without prefixing it with a colon.

This should not affect your code if you follow the Express 4 documentation of app.param, as it makes no mention of the leading colon.
req.param(name)

This potentially confusing and dangerous method of retrieving form data has been removed. You will now need to specifically look for the submitted parameter name in the req.params, req.body, or req.query object.
res.json(obj, status)

Express 5 no longer supports the signature res.json(obj, status). Instead, set the status and then chain it to the res.json() method like this: res.status(status).json(obj).
res.jsonp(obj, status)

Express 5 no longer supports the signature res.jsonp(obj, status). Instead, set the status and then chain it to the res.jsonp() method like this: res.status(status).jsonp(obj).
res.send(body, status)

Express 5 no longer supports the signature res.send(obj, status). Instead, set the status and then chain it to the res.send() method like this: res.status(status).send(obj).
res.send(status)

Express 5 no longer supports the signature res.send(status), where status is a number. Instead, use the res.sendStatus(statusCode) function, which sets the HTTP response header status code and sends the text version of the code: “Not Found”, “Internal Server Error”, and so on. If you need to send a number by using the res.send() function, quote the number to convert it to a string, so that Express does not interpret it as an attempt to use the unsupported old signature.
res.sendfile()

The res.sendfile() function has been replaced by a camel-cased version res.sendFile() in Express 5.
Changed
Path route matching syntax

Path route matching syntax is when a string is supplied as the first parameter to the app.all(), app.use(), app.METHOD(), router.all(), router.METHOD(), and router.use() APIs. The following changes have been made to how the path string is matched to an incoming request:

    Add new ?, *, and + parameter modifiers.
    Matching group expressions are only RegExp syntax.
        (*) is no longer valid and must be written as (.*), for example.
    Named matching groups no longer available by position in req.params.
        /:foo(.*) only captures as req.params.foo and not available as req.params[0].
    Regular expressions can only be used in a matching group.
        /\\d+ is no longer valid and must be written as /(\\d+).
    Special * path segment behavior removed.
        /foo/*/bar will match a literal * as the middle segment.

Rejected promises handled from middleware and handlers

Request middleware and handlers that return rejected promises are now handled by forwarding the rejected value as an Error to the error handling middleware. This means that using async functions as middleware and handlers are easier than ever. When an error is thrown in an async function or a rejected promise is awaited inside an async function, those errors will be passed to the error handler as if calling next(err).

Details of how Express handles errors is covered in the error handling documentation.
app.router

The app.router object, which was removed in Express 4, has made a comeback in Express 5. In the new version, this object is a just a reference to the base Express router, unlike in Express 3, where an app had to explicitly load it.
req.host

In Express 4, the req.host function incorrectly stripped off the port number if it was present. In Express 5, the port number is maintained.
req.query

The req.query property is no longer a writable property and is instead a getter. The default query parser has been changed from “extended” to “simple”.
Improvements
res.render()

This method now enforces asynchronous behavior for all view engines, avoiding bugs caused by view engines that had a synchronous implementation and that violated the recommended interface.