Why my admin-ajax url returns 0 even after adding echo and die() at the end of function?

Avoid using ajax URLs like this when working with ajax calls. Using wp_localize_script is a good way to accomplish this.

Let’s assume your js code is in the script.js file. You need to localize that file.

function my_theme_enqueue_scripts() {
    wp_enqueue_script( 'custom-jquery', 'https://code.jquery.com/jquery-3.5.1.min.js', array(), '1.0', true );
    wp_enqueue_script( 'ajax-script', plugin_dir_url( __FILE__ ) . 'js/script.js', array( 'jquery' ), '1.0', true );
    wp_localize_script( 'ajax-script', 'Obj', array( 'ajax_url' => admin_url( 'admin-ajax.php' ) ) );
}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_scripts' );

JS file:

jQuery(document).ready(function ($) {
  $("#submit").on("click", function (e) {
    e.preventDefault();
    let ajax_url = Obj.ajax_url;        //Ajax url
    let title = $("#title").val();
    let content = $("#content").val();
    var data = new FormData($('#postform')[0]);
    data.append( "image", $('#input-image')[0].files[0]);
    data.append( "action", 'my_ajax_handler');
    data.append( "title", $('#title').val());
    data.append( "content", $('#content').val());;

    $.ajax({
      url: ajax_url,
      type: "POST",
      data: data,
      success: function (response) {
        console.log(response);
      },
    });
  });
});

Ajax callback function:

function my_ajax_handler() {
    if ( isset( $_FILES['image'] ) ) {

        $post_title   = $_POST['title'];
        $image        = $_FILES['image'];
        $post_content = $_POST['content'];
        
        $new_post = array(
            'post_title'   => $post_title,
            'post_content' => $post_content,
            'post_status'  => 'publish',
            'post_name'    => 'pending',
        );

        $pid = wp_insert_post( $new_post );
        add_post_meta( $pid, 'meta_key', true );

        if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
            require_once ABSPATH . 'wp-admin' . '/includes/image.php';
            require_once ABSPATH . 'wp-admin' . '/includes/file.php';
            require_once ABSPATH . 'wp-admin' . '/includes/media.php';
        }
        if ( $_FILES ) {
            foreach ( $_FILES as $file => $array ) {
                if ( $_FILES[ $file ]['error'] !== UPLOAD_ERR_OK ) {
                    return 'upload error : ' . $_FILES[ $file ]['error'];
                }
                $attach_id = media_handle_upload( $file, $pid );
            }
        }
        if ( $attach_id > 0 ) {
            // and if you want to set that image as Post then use:
            update_post_meta( $pid, '_thumbnail_id', $attach_id );
        }
    }
    echo json_encode( 'success' );
    die();
}
add_action( 'wp_ajax_my_ajax_handler', 'my_ajax_handler' );
add_action( 'wp_ajax_nopriv_my_ajax_handler', 'my_ajax_handler' );

It is working for me. I would like to know if it works for you, and make sure you have included PHP file of ajax request in your main plugin file where you have enqueued JS file.