Node.js Express POST /api/checkout 路由返回404 Not Found问题
问题描述
前端向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

