Wilson Silva
Wilson Silva
All Posts Apr 13 2024

API Versioning using Cloudflare Workers

Alternatives:

  • Feature flags
  • Different controller
  • Code branching

Benefits:

  • Avoid introducing technical debt in the main codebase
  • Easy to delete the façade when the version is no longer needed

Risks:

  • The team wasn’t proficient in TypeScript and the Workers runtime

What we got right:

  • Used TypeScript
  • Had 100% automated tests, manual QA, code review, and CI/CD and left it in an integration environment used by everyone for a few weeks.
  • Had 100% Documentation
  • Had code linting
  • Had staging environments

What went wrong:

  • No logging
  • No monitoring
  • The response from the API was sometimes empty because of caching, this caused JSON parsing issues

Chat with Matt

Wilson Silva: Hello guys! Remember that time we deployed a Cloudflare Worked to convert one payload from one format to another? It crashed sometimes because of an HTTP caching issue. We did a post-mortem. What could have we done differently? Do you have that post-mortem somewhere? Matt Gibson: Can’t think where the post mortem would be, but the impossibility of local testing was an issue. It was a new tool and that hadn’t been added yet. There’s a local dev version now, so you can unit test. Also would have been good to have a test suite that ran against the staging server. Matt Gibson: Also it ate errors and you couldn’t see any logs Matt Gibson: Good shout. I have a long list of half finished blog posts. Need to get into the habit of being less perfectionist about them Matt Gibson: I think it was more that Cloudflare wasn’t set up for automatically making the logging visible. We’d assumed it would be clear when it failed and it was silent. That’s a surprising way fo their tech to work, so we were stung by that Matt Gibson: If we’d routed all traffic through a small proxy app with logging, it would have been fine Matt Gibson: But slower Matt Gibson: As I remember, we also split up the tasks amongst different team members, but it turned out many parts were doing the same thing, so we ended up with several ways of testing and implementing the same parts Matt Gibson: Doing it again, we’d have benefited from getting a single endpoint out into production early and exploring how it worked, then building on that

I got internal error when I did wrangler dev –local-upstream http://localhost:3000 instead of wrangler dev –local-upstream localhost:3000 I got an internal error

rails new versioned_blog
–api
–database=sqlite3
–no-rc
–skip-keeps
–skip-action-mailer
–skip-action-mailbox
–skip-action-text
–skip-active-job
–skip-active-storage
–skip-action-cable
–skip-asset-pipeline
–skip-javascript
–skip-hotwire
–skip-jbuilder
–skip-test
–skip-system-test
–skip-bootsnap

bundle exec rails g scaffold Post name:string body:string bundle exec rails g scaffold Tag post:references name:string

npm install wrangler –save-dev or npm install -g wrangler

https://developers.cloudflare.com/fundamentals/api/get-started/create-token/

https://dash.cloudflare.com/profile/api-tokens Click on Create Token Click on Edit Template next to Edit Cloudflare Workers Select the appropriate Account Resources Select the appropriate Zone Resources Click on Continue to Summary Click on Create Token export CLOUDFLARE_API_TOKEN=KfCAIweJ4nqCzyQuePpzUXQZzlUtvWo82lDHUL8T

V1:

/posts Post: title, body

/posts/:post_id/tags Tag: name

V2:

/posts Post: title, body, tags Tag: name

Consider this Cloudflare Worker example to modify a request:

export default { async fetch(request) { /**

  • Example someHost is set up to return raw JSON
  • @param {string} someUrl the URL to send the request to, since we are setting hostname too only path is applied
  • @param {string} someHost the host the request will resolve too */ const someHost = “example.com”; const someUrl = “https://foo.example.com/api.js”;

    /**

    • The best practice is to only assign new RequestInit properties
    • on the request object using either a method or the constructor */ const newRequestInit = { // Change method method: “POST”, // Change body body: JSON.stringify({ bar: “foo” }), // Change the redirect mode. redirect: “follow”, // Change headers, note this method will erase existing headers headers: { “Content-Type”: “application/json”, }, // Change a Cloudflare feature on the outbound response cf: { apps: false }, };

    // Change just the host const url = new URL(someUrl);

    url.hostname = someHost;

    // Best practice is to always use the original request to construct the new request // to clone all the attributes. Applying the URL also requires a constructor // since once a Request has been constructed, its URL is immutable. const newRequest = new Request( url.toString(), new Request(request, newRequestInit) );

    // Set headers using method newRequest.headers.set(“X-Example”, “bar”); newRequest.headers.set(“Content-Type”, “application/json”); try { return await fetch(newRequest); } catch (e) { return new Response(JSON.stringify({ error: e.message }), { status: 500, }); } }, };

And this example to modify a response:

export default { async fetch(request) { /**

  • @param {string} headerNameSrc Header to get the new value from
  • @param {string} headerNameDst Header to set based off of value in src */ const headerNameSrc = “foo”; //”Orig-Header” const headerNameDst = “Last-Modified”;

    /**

    • Response properties are immutable. To change them, construct a new
    • Response and pass modified status or statusText in the ResponseInit
    • object. Response headers can be modified through the headers set method. */ const originalResponse = await fetch(request);

    // Change status and statusText, but preserve body and headers let response = new Response(originalResponse.body, { status: 500, statusText: “some message”, headers: originalResponse.headers, });

    // Change response body by adding the foo prop const originalBody = await originalResponse.json(); const body = JSON.stringify({ foo: “bar”, …originalBody }); response = new Response(body, response);

    // Add a header using set method response.headers.set(“foo”, “bar”);

    // Set destination header to the value of the source header const src = response.headers.get(headerNameSrc);

    if (src != null) { response.headers.set(headerNameDst, src); console.log( Response header "${headerNameDst}" was set to "${response.headers.get( headerNameDst )}" ); } return response; }, };

I have an API. I want to make a breaking change. I’m using Cloudflare Workers to implement a Façade that will convert a request from an old format to the new format, pass it to the API and convert the new response to the old format. But only for requests whose user agent matches BlogiOS/1.x.x or BlogAndroid/1.x.x. All remaining requests should pass through with no modification to the request. In all cases, retain the headers.

Old API:

POST /posts/:post_id/tags { tags: [ { name: ‘example1’ }, { name: ‘example2’ } ] }

Response:

{“message”=>”Tags successfully updated.”}

Error response:

{“error”=>”Validation failed: Name is too short (minimum is 3 characters)”}

New API:

PUT/PATCH /posts/:post_id { post: { tags: [ { name: ‘example1’ }, { name: ‘example2’ } ] } }

Response: