denoland / fresh

The next-gen web framework.
https://fresh.deno.dev
MIT License
12.48k stars 644 forks source link

Fetching from own api route #398

Closed WittySmirk closed 2 years ago

WittySmirk commented 2 years ago

In Next.js if you had an api endpoint at '/pages/api/joke/' inside of a page you can call:

const resp = await fetch('/api/joke')

and it will fetch the data that endpoint returns.

Is there already a simple way to do this in fresh? Because so far I have had to do this weird workaround with http request headers:

//routes/test.tsx

//Fetches from the example "joke" api endpoint\
//Inside GET in handler:
const url = 'http://' + req.headers.get('host')?.toString() + '/api/joke';
const resp = await fetch(url);

Is there a built in way to get the url of the project so you can easily call your own api endpoints within the app? (like the Next.js example)

Currently I have been working with a simple utility I made that basically just returns the url given a Request object, but if there was a baked in way this would be much easier

raymclee commented 2 years ago

why not just fetching inside the page handler function? maybe create a shared function If you need the api route for other application

ajf-sa commented 2 years ago

@WittySmirk did you use IS_BROWSER should work for client side

const resp = async () => {
    if (IS_BROWSER) {
      const resp = await fetch("/api/joke");
      const json = await resp.json();
      console.log(json);
    }
  };

it happen to my when I fetch some data in Nextjs in server side you should bring absolute url

WittySmirk commented 2 years ago

@raymclee I was doing this inside a page handler to the 2.6 Fetching Data documentation @alfuhigi I was doing this server side in a page, not in an island

The workaround I used in the first post turned out to not work on deno deploy as it created a 508 loop detected error so I decided to call the handler for the api itself but this was still very technical and it feels like there should be a better way to do this. This is all the code that goes into that:

// index.tsx
import { handler as jokeHandler } from './api/joke'; //The api handler
import { GetFromHandler } from '../utils/getUrl.ts'; //My implementation for get request from a handler

interface Joke {
   text: string;
}

export const handler: Handlers = {
   async GET(req, ctx) {
      const joke: Joke = await GetFromHandler(req, ctx, jokeHandler); //This is my own implementation

      return ctx.render(joke);
   }
}

export default function Home({ data }: PageProps<Joke>){
   return <p>{data.text}</p>
}

//Inside utils/getUrl.ts
export const GetFromHandler = async (
   req: Request, 
   ctx: HandlerContext, 
   handler: (_req: Request, _ctx: HandlerContext) => Response 
) => {
   const response = handler(req, ctx);
   const decoded = new TextDecoder().decode(
      (await response.body?.getReader().read())?.value
   );

  try {
    return JSON.parse(decoded);
  } catch {
    return { text: decoded };  
  }
}

I couldn't find documentation for this anywhere I just went step by step and logged the values until I got to the actual response from the api. I know there is also a similar issue when posting data to an api from a form because you have to get the body from the http request object itself which uses Uint8 arrays to send text inside of a Readable Stream.

So is there a baked in way to do this simply or should I keep using this GetFromHandler method I've made?

talebisinan commented 2 years ago

In the latest version, you can simply call the handler function defined in the API routes.

This is the joke.ts file comes with the example:

import { HandlerContext } from "$fresh/server.ts";

// Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/
const JOKES = [
  "Why do Java developers often wear glasses? They can't C#.",
  "A SQL query walks into a bar, goes up to two tables and says “can I join you?”",
  "Wasn't hard to crack Forrest Gump's password. 1forrest1.",
  "I love pressing the F5 key. It's refreshing.",
  "Called IT support and a chap from Australia came to fix my network connection.  I asked “Do you come from a LAN down under?”",
  "There are 10 types of people in the world. Those who understand binary and those who don't.",
  "Why are assembly programmers often wet? They work below C level.",
  "My favourite computer based band is the Black IPs.",
  "What programme do you use to predict the music tastes of former US presidential candidates? An Al Gore Rhythm.",
  "An SEO expert walked into a bar, pub, inn, tavern, hostelry, public house.",
];

export const handler = (_req: Request, _ctx: HandlerContext): Response => {
  const randomIndex = Math.floor(Math.random() * JOKES.length);
  const body = JOKES[randomIndex];
  return new Response(body);
};

You can simply access this API route by using it like;

import { Handlers, PageProps } from "$fresh/server.ts";
import { handler as jokeHandler } from "./api/joke.ts";

interface Joke {
  text: string;
}

export const handler: Handlers = {
  async GET(req, ctx) {
    const res = await jokeHandler(req, ctx);
    const joke = await res.text();
    return ctx.render(joke);
  },
};

export default function Foo({ data }: PageProps<Joke>) {
  return (
    <main>
      {data && <p>{data}</p>}
    </main>
  );
}
nnmrts commented 2 years ago

Please reopen this, calling the handler directly becomes somewhat bad DX once dynamic routing is introduced, in my opinion.

Considering the handler at routes/api/projects/[slug].js looks like this:

const handler = {
    get: async(request, context) => {
            const connection = await database.connect();

            const assets = (await connection.queryObject`SELECT * FROM projects WHERE slug = ${context.params.slug}`);

            connection.release();

            return new Response(JSON.stringify(assets.rows[0]));
    };
};

export { handler };

Then locally this works "fine" and is the simplest way to request the handler:

routes/projects/[name].jsx

const handler = {
    get: async(request, context) => {
        const slug = context.params.name.toLocaleLowerCase().replace(" ", "-");

        const url = new URL(`/projects/${slug}`, env.get("BASE_URL"));

        const project = await (await fetch(url)).json();
    };
};

But on Deno Deploy (and if we want to avoid makig extra http requests in general) it suddenly explodes into this because we have to pass context params manually:

routes/projects/[name].jsx

import { handler as projectHandler } from "./api/projects/[slug].js";

const handler = {
    get: async(request, context) => {
        const slug = context.params.name.toLocaleLowerCase().replace(" ", "-");

        const project = await (
            await projectHandler(request, { ...context, params: { ...context.params, slug } })
        ).json();
    };
};

I realize that http requesting your own api is bad and that this is a very simple example but just imagine cases with sub routes or multiple dynamic parts in an api route like /api/projects/[slug]/comments or /api/projects/[slug]/images/[size].

I still feel like there should be some utility functions provided by fresh for this. At least something to avoid having to pass the "dynamic" params manually, because otherwise the purpose of using square brackets in your filenames and the correct directory structure gets lost (well unless your api is public as well, then it at least has some purpose...). Maybe something like this would be nice to have:

routes/projects/[name].jsx

import { internalRequest } from "fresh";

const handler = {
    get: async(request, context) => {
        const slug = context.params.name.toLocaleLowerCase().replace(" ", "-");

        const project = await (await internalRequest(`/api/projects/${slug}`)).json();
    };
};

internalRequest here would use fresh's built-in route resolution to then figure out that the path api/projects/example-project means "call the handler in ./api/projects/[slug].js and attach the param slug with the value example-project to it". This might seem like something a third-party library should provide (using path-to-regexp and dynamic imports, I guess), but a utility like this from fresh itself would be optimal, because then you can be sure it uses the same algorithm as its http server for resolving handler files.

EDIT: Another idea would be for fresh to globally catch every outgoing request to itself and "responding" with the result of calling the handler directly; somewhat similar to what a service worker in the browser can do. So basically every fetch that requests localhost:9000 automatically turns into await (await import("handler.js").default)(request, params);