Files
web/apps/scandic-web/app/api/destinations/[country]/[city]/route.ts
Joakim Jäderberg 15a2da333d Merged in feature/SW-3572-hotel-data-endpoint (pull request #3051)
SW-3572 API route for listing hotels per city or country

* wip hotel data endpoint

* Correct route params type

* wip

* skip static paths call

* timeout when getting destinations take too long

* call noStore when we get a timeout

* add cache-control headers

* .

* .

* .

* wip

* wip

* wip

* wip

* add route for getting hotels per country

* include city when listing by country

* fix distance SI unit

* fix sorting

* Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/SW-3572-hotel-data-endpoint

* packages/tracking passWithNoTests

* revalidate must be static value

* remove oxc reference

* cleanup

* cleanup hotel api route

* feat(SW-3572): cleanup error handling


Approved-by: Anton Gunnarsson
2025-11-03 12:10:22 +00:00

140 lines
3.8 KiB
TypeScript

import * as Sentry from "@sentry/nextjs"
import { TRPCError } from "@trpc/server"
import { unstable_noStore as noStore } from "next/cache"
import { NextResponse } from "next/server"
import { Lang } from "@scandic-hotels/common/constants/language"
import { equalsIgnoreCaseAndAccents } from "@scandic-hotels/common/utils/stringEquals"
import {
gatewayTimeout,
httpStatusByErrorCode,
notFound,
} from "@scandic-hotels/trpc/errors"
import { Country } from "@scandic-hotels/trpc/types/country"
import {
isCityLocation,
isHotelLocation,
} from "@scandic-hotels/trpc/types/locations"
import { serverClient } from "@/lib/trpc/server"
import { timeout } from "@/utils/timeout"
import { createDataResponse } from "../createDataResponse"
export const revalidate = 28_800 // 8 hours
export async function GET(
_request: Request,
{ params }: { params: Promise<{ country: string; city: string }> }
) {
try {
const { country: countryParam, city: cityParam } = await params
const country = Object.values(Country).find((c) =>
equalsIgnoreCaseAndAccents(c, countryParam)
)
if (!country) {
throw notFound(`Country "${countryParam.toLowerCase()}" not found`)
}
const caller = await serverClient()
const locations = await Promise.any([
timeout(3_000).then(() => {
throw gatewayTimeout("Fetching locations timed out")
}),
caller.hotel.locations.get({ lang: Lang.en }),
])
const city = locations.filter(isCityLocation).find((c) => {
return (
equalsIgnoreCaseAndAccents(c.name, cityParam) &&
equalsIgnoreCaseAndAccents(c.countryName, countryParam)
)
})
if (!city) {
throw notFound(
`City "${cityParam.toLowerCase()}" not found in country "${countryParam.toLowerCase()}"`
)
}
const hotels = locations
.filter(isHotelLocation)
.filter(
(x) =>
x.isActive &&
x.isPublished &&
equalsIgnoreCaseAndAccents(x.relationships.city.name, cityParam)
)
if (hotels.length === 0) {
throw notFound(
`No hotels found in city "${cityParam.toLowerCase()}" and country "${countryParam.toLowerCase()}"`
)
}
return NextResponse.json(
createDataResponse({ countryParam, cityParam, hotels }),
{
status: 200,
headers: {
"Cache-Control": `public, max-age=${revalidate}, stale-while-revalidate=86400`,
"Netlify-CDN-Cache-Control": `public, max-age=${revalidate}, stale-while-revalidate=86400`,
},
}
)
} catch (error) {
noStore()
const noCacheHeader: HeadersInit = {
"Cache-Control": `no-store, max-age=0`,
}
if (error instanceof TRPCError) {
switch (error.code) {
case "GATEWAY_TIMEOUT":
case "INTERNAL_SERVER_ERROR": {
return NextResponse.json(
{
message: error.cause?.toString() || error.message,
},
{
status: httpStatusByErrorCode(error),
headers: {
...noCacheHeader,
},
}
)
}
case "NOT_FOUND": {
return NextResponse.json(
{
message: error.cause?.toString() || error.message,
},
{
status: 404,
headers: {
"Cache-Control": `public, max-age=${revalidate}, stale-while-revalidate=86400`,
"Netlify-CDN-Cache-Control": `public, max-age=${revalidate}, stale-while-revalidate=86400`,
},
}
)
}
}
}
Sentry.captureException(error)
return NextResponse.json(
{ message: "Internal Server Error" },
{
status: 500,
headers: {
...noCacheHeader,
},
}
)
}
}