iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🗂

Building a Markdown Conversion API with Cloudflare Workers and Node.js

に公開
2

I tried running Node.js on Cloudflare Workers, so I have organized and summarized the steps here (original scrap). As an example, I will deploy an API to Cloudflare Workers that converts HTML to Markdown using Node.js + marked.

Related Articles

https://zenn.dev/catnose99/articles/d1d16e11e7c6d0

Preparation

Make sure to create a Cloudflare account.

Install Wrangler

To build, preview, and deploy functions on Cloudflare Workers, you will use the official CLI called Wrangler. Here, we will install Wrangler globally.

Installing via npm

$ npm install -g @cloudflare/wrangler

:::message warning
As of 2021/02/07, it is not possible to install @cloudflare/wrangler on M1 Macs using npm (#1685), so we will use cargo.
:::

Installing via cargo

cargo is the package manager for Rust. If you don't have cargo installed, you can install it using curl https://sh.rustup.rs -sSf | sh.

$ cargo install wrangler

The installation is now complete.

Linking your account with wrangler config

Next, link your account to the CLI. When you run wrangler config, you will be asked for an API Token. Enter the one you obtained from the Cloudflare dashboard.

About obtaining the API Token

You can issue it from a menu such as "Managing API Tokens and Keys" in the Cloudflare dashboard.

Since we are running Cloudflare Workers this time, I selected the [Edit Cloudflare Workers] template to generate the Token.

Creating a Cloudflare Workers project using a starter

There are many starters (templates) available for Cloudflare Workers.

https://developers.cloudflare.com/workers/starters

This time, we will use the TypeScript starter.

$ wrangler generate my-app https://github.com/EverlastingBugstopper/worker-typescript-template
If an error occurs on M1 Mac

As of 2021/02/06, running wrangler generate on an M1 Mac results in the following error.

Error: could not download `cargo-generate`
no prebuilt cargo-generate binaries are available for this platform

As a workaround, clone the starter's GitHub repository and place wrangler.toml in the project root. Then, refer to the documentation to specify your Cloudflare account's account_id and other settings in wrangler.toml.

This should result in the same configuration as running wrangler generate.

You should find a file named wrangler.toml at the root of the generated project. Here, you specify your Cloudflare account ID and the name of the Worker when deployed (the string that will also be part of the URL).

Check here for how to write wrangler.toml →

Create a Worker referencing Examples

Official Examples are extensive, so if you're stuck, it's a good idea to first check if there are any samples doing something similar.

https://developers.cloudflare.com/workers/examples

The following samples were particularly helpful this time:

Basic Usage

In Cloudflare Workers, you receive HTTP requests using a Fetch Listener (addEventListener('fetch', (event) => {})).

addEventListener("fetch", (event) => {
  return event.respondWith(
    new Response("Hello world")
  )
})

The event argument contains header information and parameters, and is also used when generating a response.

Simple CORS setup

If you need CORS configuration for an API that receives requests from a browser, you should respond to preflight requests (OPTIONS requests). In this case, since it is a simple API that just converts Markdown to HTML, we will allow all origins.

addEventListener('fetch', (event) => {
  const request = event.request // the request object is also included in the event

  // If the request method is OPTIONS, consider it a preflight request
  if (request.method === 'OPTIONS') {
    return event.respondWith(
      new Response(null, {
        headers: {
	  'Access-Control-Allow-Origin': '*',
	  'Access-Control-Allow-Methods': 'POST,OPTIONS', // Accept POST requests
          'Access-Control-Max-Age': '86400',
          'Access-Control-Allow-Headers': 'Content-Type'
	}
      })
    )
  }

  // ...later...
})

Returning JSON

To return JSON as a response in Cloudflare Workers, write it like this (almost exactly as shown in the official sample).

addEventListener("fetch", event => {
  const data = {
    message: "Hello!"
  }

  const json = JSON.stringify(data)

  return event.respondWith(
    new Response(json, {
      headers: {
        "content-type": "application/json;charset=UTF-8"
      }
    })
  )
})

Obtaining the POST request body

The body of a POST request can be obtained using request.json() when the Content-Type is application/json (reference sample).

addEventListener("fetch", (event) => {
  const request = event.request
  const { markdown } = await request.json()
}

Final Implementation Example

Here is an implementation sample for an API that performs the following processing.

  1. Receive markdown in the body via a POST request
  2. Convert Markdown to HTML using marked
  3. Return { html: "Converted HTML" } as JSON
index.ts
import marked from "marked"

function handleOptions(request: Request) {
  return new Response(null, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'POST,OPTIONS',
      'Access-Control-Max-Age': '86400',
      'Access-Control-Allow-Headers': 'Content-Type'
    }
  })
}

async function handlePost(request: Request): Promise<Response> {
  if (!request.headers.get('content-type')?.includes('application/json')) {
     return new Response('Invalid Content Type', {
       status: 415,
     })
  }

  const { markdown } = await request.json()
  const html = marked(markdown) // Convert Markdown to HTML
  const json = JSON.stringify({ html })

  return new Response(json, {
    headers: {
      'content-type': 'application/json;charset=UTF-8',
    },
  })
}


addEventListener('fetch', (event) => {
  const request = event.request

  // Respond to preflight requests
  if (request.method === 'OPTIONS') {
    return event.respondWith(handleOptions(request))
  }

  // Return error for non-POST requests
  if (request.method !== 'POST') {
    return event.respondWith(
      new Response('Method Not Allowed', {
        status: 405,
      }),
    )
  }

  event.respondWith(handlePost(request))
})

Now the implementation itself is complete.

Try it out on localhost

Run the following command to start a server on localhost and test your function.

$ wrangler dev
# 👂 Listening on http://127.0.0.1:8787

Deploying

Run the following command to deploy to Cloudflare Workers and start accepting requests from the outside.

$ wrangler publish

Thoughts after trying Cloudflare Workers

I'm happy that the response is fast and there seems to be no latency like a cold start. I plan to use it extensively in the future.

Discussion

JJJJ

今はNode.js側でmarkedを使って実行してるので多分同じマークダウンの量でもクライアントでやった方がhttpのオーバーヘッド分を超えるパフォーマンスが出るのかなと思ったりしてますが、これをwasmに変えてみたり、別の言語で書いてみたりしてどのぐらいでEdgeでやった方が早いみたいな分岐点が来るのか気になりますね👀


追記です
ユースケースを想定できてなかったのですが例えばgithubにあるmdをhtmlに変換するなどzennにありそうなケースの場合は、確かにCloud Functionでやってるからそこを

コールドスタートの遅さを解消したい
Cloud Functionsよりパフォーマンスが良さそう
(おそらく)料金的にも安くなる

から解決したいみたいなモチベーションですね。

管理画面的なものとか、zenn.devでclient上でpreviewをしてる部分などの変換に使うのかと思ってコメントしました

catnosecatnose

違いないですね。そのあたりを今後いろいろと試していきたいです。

Node.jsやC/Kotolin/Dartなどの対応言語でしかできない処理を任せるのにはかなり良さそうです。これまでCloudFunctionsやAWS Lambdaに任せていたことを今後は積極的にCloudflare Workersでやっていきたい気持ちです。