You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js Express POST /api/checkout 路由返回404 Not Found问题

React + Node.js/Express 集成Stripe支付时404错误修复方案

问题描述

前端向http://localhost:4000/api/checkout发送POST请求时,始终返回404 Not Found。Postman测试同样返回Cannot POST /api/checkout。

问题根源

后端server.js中通过app.use("/api/checkout", require("./routes/checkout"))挂载路由,但./routes/checkout文件里定义的POST接口路径为/checkout,导致实际可访问路径变成了/api/checkout/checkout,和前端请求的/api/checkout不匹配。

代码示例

前端fetch请求代码

const response = await fetch(`${config?.baseUrl}/api/checkout`,{
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    items: products,
    email: currentUser?.email,
  }),
})

后端路由文件./routes/checkout原代码

const express = require("express");

const router = express.Router();
const Stripe = require("stripe");

const stripeSecretKey = process.env.STRIPE_SECRET_KEY;

router.post("/checkout", async (req, res) => {
  console.log("Checkout API hit", req.body);  

  const stripe = new Stripe(stripeSecretKey, {
    apiVersion: "2025-03-31.basil",
  });
  try {
    
    const { items, email } = await req.body;

    // 将商品转换为Stripe要求的格式
    const extractingItems = await items.map((item) => ({
      quantity: item.quantity,
      price_data: {
        currency: "usd",
        unit_amount: item.discountedPrice * 100,
        product_data: {
          name: item.name,
          description: item.description,
          images: item.images,},
      },
    }));

    // 创建支付会话
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items: extractingItems,
      mode: "payment",
      success_url:"http://localhost:5173/success?session_id={CHECKOUT_SESSION_ID}",
      cancel_url: "http://localhost:5173/cancel",
      metadata: {email,},
    });

    res.json({
      message: "Server is connected",
      success: true,
      id: session.id,
    });
  
} catch (error) {
    res.status(500).json({ error: error });
  }
});

module.exports = router;

相关辅助代码

success.tsx

import { Link, useLocation, useNavigate } from "react-router-dom";
import { Store } from "../lib/store";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import Container from "../ui/Utility/Container";
import Loading from "../ui/Utility/Loading";
import { config } from "../config";

const Success = () => {
  const { currentUser, cartProduct, resetCart } = Store();
  const location = useLocation();
  const sessionId = new URLSearchParams(location.search).get("session_id");
  const navigate = useNavigate();
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!sessionId) {
      navigate("/");
      return;
    }
  
    const saveOrder = async () => {
      setLoading(true);
      try {
        const response = await fetch(`${config.baseUrl}/create-order`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            userEmail: currentUser?.email,
            userId: currentUser?.id,
            paymentId: sessionId,
            orderItems: cartProduct,
          }),
        });
  
        const data = await response.json();
        if (data.success) {
          toast.success("订单保存成功!");
          resetCart();
        } else {
          toast.error("订单保存失败。");
        }
      } catch (err) {
        console.error("保存订单错误", err);
        toast.error("保存订单失败");
      } finally {
        setLoading(false);
      }
    };
  
    if (cartProduct.length > 0 && currentUser?.email && currentUser?.id) {
      saveOrder();
    }
  }, [sessionId, navigate, currentUser, cartProduct, resetCart]);
  
  
  return (
    <Container>
      {loading && <Loading />}
      <div className="min-h-[400px] flex flex-col items-center justify-center gap-y-5">
        <h2 className="text-2xl md:text-4xl font-bold text-center">
          {loading
            ? "订单支付处理中"
            : "您的支付已被supergear.com接受"}
        </h2>

        <p>
          {loading ? "处理完成后" : "现在"}您可以查看订单或继续购物
        </p>

        <div className="flex items-center gap-x-5">
          <Link to="/orders">
            <button className="bg-black text-slate-100 w-52 h-12 rounded-full text-base font-semibold hover:bg-primeColor duration-300">
              查看订单
            </button>
          </Link>
          
          <Link to="/">
            <button className="bg-black text-slate-100 w-52 h-12 rounded-full text-base font-semibold hover:bg-primeColor duration-300">
              继续购物
            </button>
          </Link>
        </div>
      </div>
    </Container>
  );
};

export default Success;

config.ts

interface Config {
  baseUrl: string;
}
const checkConfig = (server: string): Config | object => {
  let config: Config | object = {};
  switch (server) {
    case "production":
      config = {
        baseUrl: "https://supergear1.vercel.app",
      };
      break;
    case "local":
      config = {
        baseUrl: "http://localhost:4000",
      };
      break;
    default:
      break;
  }
  return config;
};

export const selectServer = "local";
export const config = checkConfig(selectServer) as Config; // 类型断言:告诉TypeScript确定返回的对象是Config类型,即使初始为空对象。

修复方案

有两种方式可以解决路径不匹配的问题:

方案1:修改路由文件中的接口路径

将./routes/checkout里的router.post("/checkout", ...)改为router.post("/", ...),这样挂载后的路径就是/api/checkout,和前端请求一致:

// 修改后
router.post("/", async (req, res) => {
  // 原逻辑代码不变
});

方案2:修改路由挂载路径

如果不想改路由文件,也可以修改server.js中的挂载代码,将app.use("/api/checkout", require("./routes/checkout"))改为app.use("/api", require("./routes/checkout")),这样接口路径就变成/api/checkout,和前端请求匹配。

验证修复

修改完成后,重启后端服务,再用Postman或前端请求http://localhost:4000/api/checkout,应该能正常返回Stripe会话数据,不再出现404错误。


内容的提问来源于stack exchange,提问作者eslam wael

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 09:59:55