Search code examples
fetch-apisveltesveltekitpublic

How to publish a forward facing API in SvelteKit?


I know that perhaps a dedicated server would be better for this, however I'm not familiar with server only frameworks. But I'm very familiar with SvelteKit api routes. I decided to create a route named "/API" Where I have a handler class that will determine which method to run in the server based on the string passed as the func parameter in the post request. This does return the data properly when I'm using the svelte client. However, after publishing the file to Vercel It was not able to fetch the data. I will include the error message at the end since it's very long. Here is the "/API/+server.ts" file

import { json, type RequestEvent } from '@sveltejs/kit';


  export async function POST({request}:RequestEvent){
    const body = await request.json()
    const handler = new API_service(body.func);
    const params =body.params
    let data;
    if (Array.isArray(params)) {
        data = await handler.function_handler(params);
    } else {
        // Handle the case where body.params is not an array
        throw new Error("No parameters array")
    }
    
    return json( data );
}



class API_service{
    private func:string;
    private config = {
        api_key:"&x_cg_demo_api_key=Your coingecko API Key", //<--- Change accordingly
        base_url:"https://api.coingecko.com/api/v3",
        currency:"?vs_currency=usd",
        ids:"&ids=bitcoin",
        days:"&days=30"
      }
    
      
    private coins_endpoint = {
        markets:"/coins/markets",
    
      }
      
    private root_endpoint = {
        ping:"/ping"
      }



    constructor(func:string){
        this.func=func
    }
    public function_handler(params:any[]){
        switch(this.func){
            case "get_btc_ohlc":
                return this.get_btc_ohlc(params[0])

        }

    }
    private async get_btc_ohlc(id:string){
        const response = await fetch(this.config.base_url+`/coins/${id}/ohlc`+this.config.currency+this.config.days)
        return await response.json()
    }
}

I tested the endpoint with the following client side code and I got the expected response

<script>
    import {onMount} from "svelte";
    onMount(async ()=>{
        try {
            const response = await fetch('/API',{
                method:"POST",
                body:JSON.stringify({
                    func:"get_btc_ohlc",
                    params:["bitcoin"]
                })
            })
            if(!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            console.log(await response.json());
            } catch (error) {
            console.error('There was a problem with the fetch operation: ', error);
            }
    
    })
</script>

After using copilot extensively to solve this problem as well as looking online, I was unable to find a solution after several hour. Here are the suggestions copilot gave me

  1. Create a vercel.json file
  2. Create a hook.server.ts file
  3. Create a meta tag in app.html Here they are acordingly

vercel.json

{
    "headers": [
      {
        "source": "/(.*)",
        "headers": [
          {
            "key": "Content-Security-Policy",
            "value": "default-src *; connect-src 'self' https://private-coingecko-api.vercel.app; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
          }
        ]
      }
    ]
  }

hook.server.ts

import type { Handle, RequestEvent } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve } ) => {
  const response = await resolve(event);

  // Ensure response.headers exists
  if (response.headers) {
    response.headers.set(
      'Content-Security-Policy',
      "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://private-coingecko-api.vercel.app;"
    );
  }

  return response;
};

The Meta tag in app.html

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://private-coingecko-api.vercel.app;">

The following snippet is of the actual fetch to my endpoint which returns the error following it (I Ran this from the console in the default edge blank page)

await fetch("https://private-coingecko-api.vercel.app/API",{
    method:"POST",
    body:JSON.stringify({
        func:"get_btc_ohlc",
        params:["bitcoin"],
        apikey: "Your api key" //<-- I removed the middleware in the previous snippet but its still there in production
    })
})
.catch(error => console.error('Error:', error));

Error caused by fetch from the console

Here is another error from postman

Postman error message from production api

Here are the Logs from my vercel server. As you can see, the data from coingecko works just fine, however it fails to complete because of the cross site POST request. I modified my hooks file accordingly to a tutorial and here is how it looks, and it's still not working on postman. Vercel http request logs

import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ resolve, event }) => {

  // Apply CORS header for API routes
  if (event.url.pathname.startsWith('/API')) {
    // Required for CORS to work
    if(event.request.method === 'OPTIONS') {
      return new Response(null, {
        headers: {
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': '*',
        }
      });
    }
  }

  const response = await resolve(event);
  if (event.url.pathname.startsWith('/API')) {
        response.headers.append('Access-Control-Allow-Origin', `*`);
  }
  return response;
};

Edit: I'm currently reading this, it seems useful so far I still haven't figured it out https://www.programonaut.com/cross-site-post-form-submissions-are-forbidden-in-sveltekit/

Here is more detailed documentation about this topic https://kit.svelte.dev/docs/adapter-node

this information also seems relevant https://snippets.khromov.se/configure-cors-in-sveltekit-to-access-your-api-routes-from-a-different-host/


Solution

  • Thanks to the help of the user Peppe L-G I was able to find a way to fix this. I'm unsure if the hooks.server.ts file is essential for this to work, as well as the vercel.json file and the meta tag. But what finally made it work was changing the csrf attribute inside the kit attribute of the svelte.config.js file.

    import adapter from '@sveltejs/adapter-auto';
    import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
    
    /** @type {import('@sveltejs/kit').Config} */
    const config = {
        preprocess: vitePreprocess(),
    
        kit: {  
            
            adapter: adapter(),
            csrf:{
                checkOrigin:false //<--- Add this line
            }
        }
    };
    
    export default config;
    

    This allows you to make cross site POST request to your server's API. This is useful if you're trying to build a public facing API in SvelteKit. The reason I had this issue to begin with was because coingecko blocks signals coming from Google servers. So when trying to consume their API via a URLFetchApp.fetch it would not accept the request. So instead I created this proxy server in SvelteKit to triangulate the fetch and now it works. Here is the working example.

    Fetching data from coingecko using Google App Scripts and a proxy

    Please don't try to fetch my server as Middlewares will reject your request