Code Monkey home page Code Monkey logo

itty-router-openapi's Introduction

itty-router-openapi

OpenAPI 3 and 3.1 schema generator and validator for Cloudflare Workers


Documentation: https://cloudflare.github.io/itty-router-openapi/

Source Code: https://github.com/cloudflare/itty-router-openapi/


itty-router-openapi is a library that extends itty-router, a powerful and lightweight routing system for Cloudflare Workers, already familiar to many developers, and adds an easy-to-use and compact OpenAPI 3 and 3.1 schema generator and validator.

itty-route-openapi can also have class-based routes, allowing the developer to quickly build on top of and extend other endpoints while reusing code.

The key features are:

A template repository is available at cloudflare/workers-sdk, with a live demo here.

Why create another router library for workers?

This framework built on top of itty-router and extends some of its core features, such as adding class-based endpoints. It also provides a simple and iterative path for migrating from old applications based on itty-router.

Building APIs and maintaining good documentation on the parameters and fields of your API hard. However, there is an open standard that makes this documentation process much more effortless, called OpenAPI.

OpenAPI "defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection." This allows other machines to reliably parse those definitions and use the remote APIs easily, without additional implementation logic.

Some of the top requirements for Radar 2.0 were having better API documentation, improving the deployment lifecycle with end-to-end testing, and making it publicly available to Cloudflare customers. OpenAPI support quickly jumped out as the obvious choice to help us on all these fronts.

However, we struggled to find an existing OpenAPI framework that checked all our boxes:

  • Easy integration with Cloudflare Workers
  • Input validation for endpoints parameters
  • Actual code-based schema generation (not just generated from comments or manually)

Since we couldn't find anything that suited us, as many engineers do, we opted for the second-best alternative: building our own and open-source it.

Quick setup

Get started fast using the create-cloudflare command line, just run this command to setup an initial project with some example routes:

npm create cloudflare@latest hello-world -- --type openapi

---> 100%

Then to start the local server just run

cd hello-world
wrangler dev

Installation

npm i @cloudflare/itty-router-openapi --save

---> 100%

Example

Let's create our first class-based endpoint called TaskFetch in src/tasks.ts now.

Make sure that ‘Task' is global, otherwise you must redefine responses.schema.task with every endpoint.

When defining the schema, you can interchangeably use native typescript types or use the included types to set required flags, descriptions, and other fields.

import {
  OpenAPIRoute,
  Path,
  Str,
  DateOnly,
  DataOf,
} from '@cloudflare/itty-router-openapi'

const Task = {
  name: new Str({ example: 'lorem' }),
  slug: String,
  description: new Str({ required: false }),
  completed: Boolean,
  due_date: new DateOnly(),
}

export class TaskFetch extends OpenAPIRoute {
  static schema = {
    tags: ['Tasks'],
    summary: 'Get a single Task by slug',
    parameters: {
      taskSlug: Path(Str, {
        description: 'Task slug',
      }),
    },
    responses: {
      '200': {
        description: 'Task fetched successfully',
        schema: {
          metaData: {},
          task: Task,
        },
      },
    },
  }

  async handle(
    request: Request,
    env: any,
    context: any,
    data: DataOf<typeof TaskFetch.schema>
  ) {
    // Retrieve the validated slug
    const { taskSlug } = data.params

    // Actually fetch a task using the taskSlug

    return {
      metaData: { meta: 'data' },
      task: {
        name: 'my task',
        slug: taskSlug,
        description: 'this needs to be done',
        completed: false,
        due_date: new Date().toISOString().slice(0, 10),
      },
    }
  }
}

Now initialize a new OpenAPIRouter, and reference our newly created endpoint as a regular ‘itty-router’ .get route:

import { OpenAPIRouter } from '@cloudflare/itty-router-openapi'
import { TaskFetch } from './tasks'

const router = OpenAPIRouter()
router.get('/api/tasks/:taskSlug/', TaskFetch)

// 404 for everything else
router.all('*', () => new Response('Not Found.', { status: 404 }))

export default {
  fetch: router.handle,
}

Finally, run wrangler dev and head to /docs our /redocs with your browser.

You'll be greeted with a beautiful OpenAPI page that you can use to test and call your new endpoint.

Tutorial Example Preview

Pretty easy, right?

Feedback and contributions

itty-router-openapi aims to be at the core of new APIs built using Workers and define a pattern to allow everyone to have an OpenAPI-compliant schema without worrying about implementation details or reinventing the wheel.

itty-router-openapi is considered stable and production ready and is being used with the Radar 2.0 public API.

Currently this package is maintained by the Cloudflare Radar Team and features are prioritized based on the Radar roadmap.

Nonetheless you can still open pull requests or issues in this repository and they will get reviewed.

You can also talk to us in the Cloudflare Community or the Radar Discord Channel

itty-router-openapi's People

Contributors

celso avatar chitalian avatar connexinwill avatar dependabot[bot] avatar dotjs avatar ericlewis avatar erjanmx avatar g4brym avatar gimenete avatar henrytoone avatar iann838 avatar isbecker avatar juliangehring avatar kwhitley avatar leaysgur avatar marceloverdijk avatar martinnormark avatar ncabetecf avatar oscaramos avatar rmzaoo avatar sefirost avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

itty-router-openapi's Issues

Response without schema

Is it possible to define a response that has no schema, e.g. for return an error?

Currently, the schema needs to be set, for example like

responses: {
    '415': {
        description: 'Not supported',
        schema: {}
    }
}

which renders an empty json object in the schema on the documentation page.

Omitting the schema or setting it to null fails with

Uncaught TypeError: Cannot read properties of undefined (reading 'generated')
at worker.js:268:20 in getType
at worker.js:265:126 in x
at worker.js:327:5 in j
at worker.js:442:20 in getParsedSchema
at worker.js:393:21
at worker.js:709:8

It would be nice if one could represent that nothing gets returned.

lacking type safety in the openapi route object

the static schema does not match the the request type, and does not have typing on the handle parameters

makes this library really hard to use

it's also not clear how to describe the parameters!

and there are multiple places where params can be defined!
image

need to make better use of typescript for this to be nice to use

i would also recommend the schema not being static so you don't have to compute the schema and construct everything until the route is hit

TypeError: o3.replace is Not a Function - Project Not Running

Description:

I have been working on a project using itty-router-openapi, following the guide provided in the official documentation. The project was functioning correctly until recently. However, as of today, I am encountering an error that prevents the project from running.

Error Encountered
The specific error I am facing is:

TypeError: o3.replace is not a function
This error occurs when I try to run the project. I have not made any major changes to the codebase that might have led to this issue. It seems to have arisen spontaneously.

Steps to Reproduce

  1. Set up a project following the steps in the itty-router-openapi guide.
  2. Run the project.
  3. Expected Behavior
  4. The project should run without any errors, as it was doing previously.

Actual Behavior
The project fails to run, displaying the error message: TypeError: o3.replace is not a function.

Additional Information
It seems that this issue is new, as the project was working fine until today.
No significant changes were made to the codebase or the environment prior to the error occurring.

Screenshot
image

@G4brym Can you please check

When using `contentType` for `requestBody` the it is shown Example values (default)

When using the following:

const SignUpResponse = {
    message: "User Added!"
}

export class SignUp extends OpenAPIRoute {
    static schema = {
        tags: ["Users"],
        summary: "User Sign Up",
        requestBody: {
            contentType: 'text/plain',
            firstname: new Str({
                description: "User Firstname",
                required: true,
                example: "Ashish"
            }),
            lastname: new Str({
                description: "User Lastname",
                required: true,
                example: "Jullia"
            }),
            email: new Str({
                description: "User Email",
                required: true,
                example: "[email protected]"
            }),
            password: new Str({
                description: "User Password",
                required: true,
                example: "As@121212121212"
            })
        },
        responses: {
            "200": {
                contentType: 'application/json',
                schema: {
                    Response: SignUpResponse,
                },
            },
        },
    };

    async handle(
        request,
        env,
        context,
        data
    ) {
        // Retrieve the validated slug
        const { firstname, lastname, email, password } = data.body

        // console.log(isValidEmail(email))


        return {
            message: "User Added!"
        };
    }
}

It shows:
image

image

I know I'm missing something but how to correctly pass this contentType and where?

non-default base doesn't play nicely with Swagger

When using a non-root/non-default base value, the Swagger UI seems to ignore what we set.

const router = OpenAPIRouter({
	base: '/plans'
})

i.e. in the example above, Swagger's web UI attempts to make requests to / instead of /plans.

Related: #17

Support for Valibot types

Are there any plans to support Valibot for parameter types in the future?
It's a Zod alternative and is much more light weight - the same goal as itty-router

Failed to load API definition.

If the main router has base set, going to /<base>/docs renders

failed-to-load-api-def

and /<base>/redocs is broken too. Removal of the base configuration renders the pages.

The openapi.json file does load directly at /<base>/openapi.json though.

Using the workers-sdk openapi example

import { OpenAPIRouter } from '@cloudflare/itty-router-openapi';
import { TaskCreate, TaskDelete, TaskFetch, TaskList } from './tasks';

const router = OpenAPIRouter({
  base: "/tasker",
  openapi_url: '/tasker/openapi.json',
  schema: {
    info: {
      title: 'Worker OpenAPI Example',
      version: '1.0',
    },
  },
});

router.get('/api/tasks/', TaskList);
router.post('/api/tasks/', TaskCreate);
router.get('/api/tasks/:taskSlug/', TaskFetch);
router.delete('/api/tasks/:taskSlug/', TaskDelete);

// Redirect root request to the /docs page
router.original.get('/', request => Response.redirect(`${request.url}/docs`, 302));
    // Added `/` above or it redirects to `/<base>docs` not `/<base>/docs`
    
// 404 for everything else
router.all('*', () => new Response('Not Found.', { status: 404 }));

export default {
  fetch: router.handle,
};

All other routes continue to work when base is set.

routes-work

Setting docs_url or openapi_url didn't change the above result. Tested with 0.1.0 -> 0.1.6.

File Upload Demo

hello,

Content-Type: multipart/form-data

how can I upload files with Content-Type: multipart/form-data?

Cloudflare Workers Vitest integration Broken by this package

The Cloudflare Workers recommended testing strategy, the Vitest Integration, is not compatible with this package. It's due to a dependency, @asteasolutions/zod-to-openapi. This package depends on v5 of @asteasolutions/zod-to-openapi, which was not published in ESM format. Vitest only works with ESM code.

The dependency has already been fixed in v7. Can this package be updated to use the v7 of @asteasolutions/zod-to-openapi?

References

Allow response headers specification

OpenAPI spec allows the response object to specify headers https://spec.openapis.org/oas/v3.1.0#response-object

{
  "description": "A simple string response",
  "content": {
    "text/plain": {
      "schema": {
        "type": "string",
        "example": "whoa!"
      }
    }
  },
  "headers": {
    "X-Rate-Limit-Limit": {
      "description": "The number of allowed requests in the current period",
      "schema": {
        "type": "integer"
      }
    },
    "X-Rate-Limit-Remaining": {
      "description": "The number of remaining requests in the current period",
      "schema": {
        "type": "integer"
      }
    },
    "X-Rate-Limit-Reset": {
      "description": "The number of seconds left in the current period",
      "schema": {
        "type": "integer"
      }
    }
  }
}

I see that I can specify request headers with itty-router-openapi, but not the response headers. With the following definition, I can find "x-foo" request header in generated openapi.json, but not "x-bar" response header.

export class GetResult extends OpenAPIRoute {
  static schema = {
    tags: ['Results'],
    summary: 'Get operation result',
    headers: {
      "x-foo": new Str({ example: "foo-example" }),
    },
    parameters: {
      ...JSONAcceptHeader,
      id: Path(Str, {
        description: 'Result ID',
        example: '61c9e3c4-9cb2-4fb9-95a5-b48b3772741d',
      }),
    },
    responses: {
      '200': {
        contentType: 'audio/mpeg',
        schema: new Str({ format: 'binary' }),
        headers: {
          "x-bar": new Str({ example: "bar-example" }),
        },
    ...

I'm seeing the response type RouteResponse doesn't have a headers field

export declare type RouteResponse = Omit<ResponseConfig, 'content'> & {
  schema?: Record<any, any>
  contentType?: string
}

Is this something one can do in itty-router-openapi? If not, is that something that can be added?

itty-router v4.x API changes coming!

Just a heads up...

This shouldn't break anything on your end, but there are a bunch of quality of life enhancements coming to itty-router (core) very soon that you may want to include in your documentation. The more boilerplate we can remove, the easier all these libs are for folks to adopt! :)

kwhitley/itty-router#148

Security on nested routers

Reading the Swagger Bearer Authentication documentation, it is possible to add security globally, or on a specific route or routes (e.g. in the /private routes in the below screenshot.)

This doesn't appear to apply to a nested router such as the Secure section below. This nested router includes the security configuration as part of the schema but is seemingly ignored as no lock appears on the route, nor is security listed in the openapi.json for those routes (unlike the /private routes.)

Is (the inability of) applying security to all routes inside a nested router a limitation of itty-router-openapi, OpenAPI itself, or am I perhaps going about this the wrong way?

Repository: https://github.com/jasiqli/itty-openapi-security-test

docs-page

Is is possible to access `data.body` when using middlewares?

Hi,

I've the following code in which I'm trying to run a middleware on data.body.email but unable to do so, am I missing something or is it just possible with request.

index.js

import { OpenAPIRouter } from "@cloudflare/itty-router-openapi";
import { SignUp } from "./Users/controller";
import isValidEmail from "./utils/email"

const router = OpenAPIRouter({
	schema: {
		info: {
			title: "Worker OpenAPI Example",
			version: "1.0",
		},
	},
});

router.post("/api/users", isValidEmail, SignUp)

// Redirect root request to the /docs page
router.original.get("/", (request) =>
	Response.redirect(`${request.url}docs`, 302)
);

// 404 for everything else
router.all("*", () => new Response("Not Found.", { status: 404 }));

export default {
	fetch: router.handle,
};

./utils/email.js

const EMAIL_REGEXP = new RegExp(
    /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
);

const isValidEmail = (request,
    env,
    context,
    data) => {
    console.log(data)
    return EMAIL_REGEXP.test(data.body.email);
}

export default isValidEmail

./users/controller.js

import {
    Bool,
    OpenAPIRoute,
    Path,
    Str
} from "@cloudflare/itty-router-openapi";

import isValidEmail from "../utils/email"

const SignUpResponse = {
    message: "User Added!"
}

export class SignUp extends OpenAPIRoute {
    static schema = {
        tags: ["Users"],
        summary: "User Sign Up",
        requestBody: {
            contentType: 'application/json',
            firstname: new Str({
                description: "User Firstname",
                required: true,
                example: "Ashish"
            }),
            lastname: new Str({
                description: "User Lastname",
                required: true,
                example: "Jullia"
            }),
            email: new Str({
                description: "User Email",
                required: true,
                example: "[email protected]"
            }),
            password: new Str({
                description: "User Password",
                required: true,
                example: "As@121212121212"
            })
        },
        responses: {
            "200": {
                contentType: 'application/json',
                schema: {
                    Response: SignUpResponse,
                },
            },
        },
    };

    async handle(
        request,
        env,
        context,
        data
    ) {
        // Retrieve the validated slug
        const { firstname, lastname, email, password } = data.body

        // console.log(isValidEmail(email))

        return {
            message: "User Added!"
        };
    }
}

Query parameter extraction Incomplete with Question Mark (?)

The extractQueryParameters function may not be returning all query parameters in scenarios where a parameter value contains a question mark (?).

For a demonstration, please refer to and execute the code provided here.

export function extractQueryParameters(request: { url: string }): Record<string, any> {
  const url = decodeURIComponent(request.url).split('?')

  if (url.length === 1) {
    return {}
  }

  const query = url[1]

  const params: Record<string, any> = {}
  for (const param of query.split('&')) {
    const paramSplitted = param.split('=')
    const key = paramSplitted[0]
    const value = paramSplitted[1]

    if (params[key] === undefined) {
      params[key] = value
    } else if (!Array.isArray(params[key])) {
      params[key] = [params[key], value]
    } else {
      params[key].push(value)
    }
  }

  return params
}

const baseURL = "http://example.com?"
const passingCase = baseURL + encodeURIComponent("question=What is the answer to life, the universe and everything&answer=42")
const failingCase = baseURL + encodeURIComponent("question=What is the answer to life, the universe and everything?&answer=42")

console.log("URLs")
console.log("Passing case url: ", passingCase)
console.log("Failing case url: ", failingCase)

console.log("\nQuery Parameters")
console.log("Passing: ", extractQueryParameters({ url: passingCase }))
console.log("Failing: ", extractQueryParameters({ url: failingCase }))

Is this package maintained?

I see that PRs are generally merged, but there are quite a few open issues.
The statement is that this package is used in production in Cloudflare Radar.
Radar is fairly simple in terms of API complexity - it has quite a few endpoints, but all of them are just GET requests used to fetch data.
Many of the open issues evolve around more complex scenarios - different content types, empty responses, file upload, custom headers, etc.
So my question is this - is this the intention of the package owners to support wider use cases than what Radar needs?
Does the team actively investing time in it, or only if it happens to be needed by a feature in Radar?
I really like the package, but I prefer knowing upfront what I'm getting into.
Thanks

oneOf support

Is there a way to use the oneOf attribute on the response specification?

Like this:

export class PreviewLink extends OpenAPIRoute {
  static schema: OpenAPIRouteSchema = {
    tags: ["Link Preview"],
    summary: "Previews the contents of a given link",
    requestBody: {
      description: "Preview request options",
      required: true,
      content: {
        "application/json": {
          schema: PreviewLinkRequestData,
        },
        "application/x-www-form-urlencoded": {
          schema: PreviewLinkRequestData,
        },
        "multipart/form-data": {
          schema: PreviewLinkRequestData,
        },
      },
    },
    responses: {
      "200": {
        description: "Returns the link preview",
        schema: {
          oneOf: [
            ResponseDocLink,
            ResponseXUserLink,
            ResponseXPostLink,
            ResponseYoutubeChannelLink,
            ResponseYoutubeVideoLink,
            ResponseHtmlLink,
            ResponseImageLink,
            ResponseVideoLink,
            ResponseAudioLink,
            ResponseInstagramUserLink,
            ResponseInstagramPostLink,
          ],
        },
      },
    },
  };

OpenAPI Version 3.1.0

Currently this generates OpenAPI version 3.0.2, it would be cool if it generated 3.1.0. Thank you for the library friends!

Is there any way to convert Schema into typescript interface

I have this Schema

const AddUserToWaitlistSchema = {
  email: new Str({ required: true }),
  isVerfied: new Bool({ required: true }),
};

I want to convert it into an interface (IAddUserToWaitlist). Any easy way? without writing it manually

export async function addUserToWaitlist(payload: IAddUserToWaitlist) {
  try {
    console.log(payload.email);
    return {
      message: "success",
    };
  } catch (error) {
    logger.error("Error while addUserToWaitlist()", error);
    if (error instanceof DBConnectionError) {
      throw error;
    } else {
      throw new AddUserToWaitlistError(
        ADD_USER_TO_WAITLIST_ERROR.message,
        ADD_USER_TO_WAITLIST_ERROR.errorCode,
        500
      );
    }
  }
}

@G4brym @ericlewis @gimenete ?

Accessing openapi.json on prefixed path?

So in this example, I have set my wrangler.toml registered to listen on legit.website.example/plans/*, and my router registered on /plans/plans.json.

The issue here is that in production, I cannot access legit.website.example/openapi.json; how can I instead have the schema at like legit.website.example/plans/openapi.json?

name = "tls-breaker-worker-dev"
main = "src/index.js"
compatibility_date = "2022-11-23"
workers_dev = false

[vars]
ENVIRONMENT = "dev"

[env.production]
name = "tls-breaker-worker"
route = { pattern = "legit.website.example/plans/*", zone_name="website.example" }
[env.production.vars]
ENVIRONMENT = "production"
import { OpenAPIRouter, OpenAPIRoute, Query, Int } from '@cloudflare/itty-router-openapi'

const router = OpenAPIRouter()

export default {
	fetch: router.handle
}

export class Plans extends OpenAPIRoute {
	static schema = { 
		tags: ['pricing', 'prices', 'plan', 'plans'],
		summary: "Get the list of products.",
		parameters: {
			product: Query(Int, {
				description: 'Product number',
				default: 1,
				required: false,
			  }),
		}
	}

	async handle(request, env, ctx, data) {
		const { product }  = data;
		console.debug('------');
		console.debug(currency);

		return new Response("Hello world", {
			status: 200
		})
	}
}

router.get('/plans/plans.json', Plans);
router.all('*', () => new Response('Not Found.', { status: 404 }))

Path parameters not unescaped correctly

Not really sure if it's a bug with itty-router or itty-router-openapi but it affects itty-router-openapi users.

Given an endpoint like this (using .original for simplicity):

router.original.get(
  "/something/:id",
  (request) => new Response(`id: ${request.params.id}`)
);

Making a query to /something/user%3A1234 returns the following response: id: user%3A1234. While it should be id: user:1234.

Just to make sure my assumption is correct I made an express application with a similar endpoint and the response is correct (id: user:1234):

app.get("/something/:id", (req, res) => {
  res.send(`id: ${req.params.id}`);
});

My workaround is to decode the full URL before passing it to the library. Like this:

export async function handleRequest(
  request: Request,
  env: Bindings,
  context: ExecutionContext
) {
  const req = new Request(decodeURIComponent(request.url), request);
  return router.handle(req, env, context);
}

Not sure if it's the best approach.

Debugging doesn't work

Debug via breakpoints doesn't seem to be working properly with async handle or execute.

	async execute(request: Request) {
		const response = await doSomething(request);
		return response;
	}

Setting a breakpoint at const response = await doSomething(request); will pause but the response is returned (using Postman) and I can't step over to the next line.

Seems like this needs an async/await (maybe?)

Put an example of usage with modules syntax

Based on an example of the itty router library I was doing:

const router = OpenAPIRouter();
// ...
const worker: ExportedHandler<Bindings> = { fetch: router.handle };
export default worker;

By doing this, even if the parameters were correct in a request, I was getting always an empty data argument.

I had to check the code of the library and realized that if you do that then the data argument for the handle methods is not the second argument, but the fourth, because the library passes as many arguments as the router.handle function receives, plus the data. It turns out that the fetch function receives not just the Request argument, but two additional ones: an Env and an ExecutionContext.

This works fine:

const router = OpenAPIRouter();
// ...

const worker: ExportedHandler<Bindings> = {
  fetch: (request /*, env, ctx */) => router.handle(request),
};

export default worker;

Thanks!

Empty body on post request

Following the example code for requestBody I get an empty body in the handle function:

define the post like so:

router.post('/share', routes.ShareGPT)

Then my class:

export class ShareGPT extends OpenAPIRoute {
  static schema = {
    tags: ['sharegpt'],
    summary: 'Pass a JSON formatted conversation and receive a URL response from shareGPT',
    requestBody : {
      avatarUrl: new Str({
        description: 'Avatar URL',
        example: 'https://sharegpt.com/img/logo.png',
        required: false
      }),
      items: [
        {
          from: fromEnum,
          value: new Str({
            description: 'HTML formatted single message text',
            example: 'Hello, how are you?'
          }),
        },
      ] 
    },
    responses: {
      '200': {
        schema: {
          url: 'https://shareg.pt/1234567890'
        },
      },
    },
  }

  async handle(request: Request, data: Record<string, any>) {
    const {body} = data

    const shareID = '123123'
    return json({url: `https://shareg.pt/${shareID}`})
  }
} 

and body and data are both empty.

What am I doing wrong?

Arr description missing from generated schema

The description I'm defining for an Arr is missing from the generated schema.

I'm defining my request body like this:

  static schema = {
    operationId: `make-${ShutTheBoxGame.id}-move`,
    tags: TAGS,
    summary: 'Make a move in a game of Shut The Box.',
    parameters: IdParams,
    requestBody: {
      gameId: new Uuid({
        description: 'The ID of the game.',
        required: true,
      }),
      numbersToFlip: new Arr(
        new Num(),
        {
          description: 'An array of numbers the player wants to put down. Not required for a new game. If specified, gameId must be specified as well.',
          required: true,
        }),
    },
    responses: Responses,
  }

Note the description defined for numbersToFlip above in the params. This does not appear in the resulting schema:

{
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "gameId": {
                  "type": "string",
                  "description": "The ID of the game.",
                  "format": "uuid",
                  "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
                },
                "numbersToFlip": {
                  "type": "array",
                  "items": {
                    "type": "number"
                  }
                }
              },
              "required": [
                "gameId",
                "numbersToFlip"
              ]
            }
          }
        }
      }

Is this expected behavior? Am I defining the description correctly? If I'm reading the source right, perhaps getValue in the Arr class needs to merge in the value from its parent BaseParameters? https://github.com/cloudflare/itty-router-openapi/blob/main/src/parameters.ts#L72

Set default values

I can define default values in the OpenAPI schema but it would be nice for the route to set them automatically.

Can itty use OAS 3.1 directly?

We use an API-design-first approach where we design all our APIs resulting in complete OAS schemas ready for developers to use.

Instead of using

static schema = {
    tags: ['ToDo'],
    summary: 'List all ToDos',
    parameters: {
      page: Query(Int, {
        description: 'Page number',
        default: 1,
        required: false,
      }),
    },
    responses: {
      '200': {
        schema: {
          currentPage: 1,
          nextPage: 2,
          results: ['lorem'],
        },
      },
    },
  }

I was hoping to do something like

import openapi from "openapi.json"; // Obtained from our library of new and existing OAS schemas
static schema = openapi.paths["/todos"].get;

Unfortunately, I get Uncaught TypeError: Cannot read properties of undefined (reading 'generated') which as far as I can tell is because the full OAS 3.1 spec is more verbose than the itty expectation. For instance, a simple /users endpoint has a response

        "responses": {
          "200": {
            "description": "User details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "username"
                  ],
                  "properties": {
... snip

Trimming out content and application/json, and adjusting the closing braces, works. Similarly, the itty parameters don't follow OAS either since OAS doesn't generate this syntax.

      page: Query(Int, {
        description: 'Page number',
        default: 1,
        required: false,
      }),
    },

Other than manually editing very extensive OAS files, is there any workaround? Are there any plans to support OAS properly in the future?

router deep refactoring discussion

This issue is for discussing the refactoring of this project to resolve issues and improve the developer experience. Here will be laid out topics and their potential improvements if any.

Legacy schema parameters

A major part of the developer experience involves type safety and IDE code auto-completion. Legacy parameters were not implemented in a way to support these improvements. In #130, an attempt to type legacy parameters ended up having to force type overwrites using as unknown as ...

  • Removing legacy parameters altogether and using zod types instead.

However, coercion in zod is rather tricky, z.coerce.number() means Number(...), an empty parameter will coerce to 0 instead of throwing a bad request with "parameter required", things are even worse with z.coerce.boolean(). My suggestion for fixing this is to preprocess the input with JSON parse only for z.number() and z.boolean() as these explicitly require coercion. Two ways to approach this:

  • Wrap z.preprocess(...) when the input type is z.number() and z.boolean() used with Query, Path, and Header.
  • Extend zod with a constant similar to z.coerce that wraps JSON parse preprocessing. This approach however requires developers to define them explicitly.

Unused handle parameters

Most of the time due to the existence of the data parameter in handle, the previous 3 parameters are very likely to be left unused:
image

  • Instead of sequential arguments, they can be bundled into an object and later unpacking only what is needed handle({ req }, data) ....

Middleware

Currently, middleware functionality is limited, for example, the example provided in: https://cloudflare.github.io/itty-router-openapi/user-guide/middleware/. How many instances would developers like to have a User object with the authenticated user info accessible within handle?

A popular Python web framework FastAPI provides a solution by forcing middleware to be just "middleware" (like CORS, other general checks, etc.) and approaches authentication and other similar logic using Dependencies.

Repeated explicit typings

Due to how JS is implemented and how the TS compiler works, implicitly (or infer) typing a class method before it is defined is not possible, thus, developers have to explicitly type each of the parameters of the handle method after it is supposed to be typed already in schema, this also applies for the env parameter of handle. While writing extra typing may not be a problem, it could become a source of errors because it is explicit and not type-safe.

To improve this experience, it is required to make route definitions to be function-based. While I agree that class-based route definition is unique for this project, my argument is that it can still be unique with function-based routes. JS classes are just syntax sugar after all. I have a rather different syntax that I want to propose heavily inspired by FastAPI, although the background logic will mostly remain the same, the surface identify would shift dramatically:

router.post("/accounts/:accountId/todos/", {
    tags: ["Todos"],
    summary: "diruhgoerhgoie",
    description: "sroigheriog",
    deprecated: false,
    statusCode: 200,
    includeInSchema: true,
    responseClass: JSONResponse,
    responses: [
        Responds(200, "application/json", z.object({...})),
        Responds(400, "application/json", z.object({...})),
        Responds(401, "application/json", z.object({...})),
    ],
    parameters: {
        accountId: Path(z.coerce.bigint()),
        trackId: Query(z.string()),
        accept: Header(z.string()),
        todoItem: Body(z.object({
            id: z.string(),
            title: z.string(),
            description: z.string(),
        })),
    },
    dependencies: {
        session: Depends(authSession),
    }
},
({ req, env, ctx }, { accountId, trackId, accept, todoItem }) => {
    return new Response(accountId)
})

Support for minLength / maxLength strings

It'd be great to have support for minLength and maxLength.

Specially when you have a limit for a column in the database, it'd be nice to have the API to limit the value already and not have to do it manually inside the endpoint implementation.

This would also be useful for checking that a value is not an empty string by using minLength: 1, which is pretty common, I think.

How to Use Itty Router OpenAPI with Next.js?

Next with itty-router-openapi

I'm currently exploring the possibility of integrating itty router openapi into a Next.js project. My main concern revolves around the fact that Next.js comes with its own built-in routing system. I'm curious about how these two can coexist or if it's even feasible to implement itty router openapi within the Next.js environment.

Security not applying to the securitySchemes or schema

I have added an api key to my router so that when you go to docs a new authorise button appears, however when you try to make a request, it does not apply the API key you specify into the headers as expected, how do I fix this? I want to be able to have the docs show the same security etc as the main API.

export const router = OpenAPIRouter({
	docs_url: "/",
	schema: {
		info: {
			title: "JCoNet LTD API",
			version: "1",
		},
	},
});
router.registry.registerComponent("securitySchemes", "api_key", {
	type: "apiKey",
	in: "header",
	name: "x-api-key",
});

image
As you can see in the code the scheme is shown but is not being included in the docs examples so isn't applying the header, have I done something wrong or does the docs not display the headers you provide in authorise?

Non-JSON request body causes error

It seems that the router tries to parse and validate non-JSON request body and fails.

Example route:

export class ExampleRoute extends OpenAPIRoute {
    static schema = {
        // ...
        requestBody: {
            contentType: 'application/octet-stream'
        },
        // ...
    }
}

Error response:

{
  "errors": {
    "body": "Unexpected token 'R', \"Received: \"... is not valid JSON"
  },
  "success": false,
  "result": {}
}

The only way to avoid this is not to specify requestBody at all. In that case, however, /docs gets broken and doesn't allow to submit file as a request body.

Allowing multiple content types for the same response status code

I'm looking to have a conditional response content type (for example based on the request's "Accept" header value).

If the client asks for "application/json", I want to return

    '200': {
      contentType: 'application/json',
      schema: new Obj({
          id: new Str({ example: "123" }),
          attributes: new Obj({
              timestamp: new Str({ example: '2020-08-01T18:41:00.000Z' }),
          }),
        }),
    },

If the client asks for "audio/mpeg", I want to return

      '200': {
        contentType: 'audio/mpeg',
        schema: new Str({ format: 'binary' }),

If I'm correct, the current code only allows one content type per status code (src/types.ts)

export declare type RouteResponse = Omit<ResponseConfig, 'content'> & {
  schema?: Record<any, any>
  contentType?: string
}

export declare type OpenAPIRouteSchema = Omit<
  RouteConfig,
  'method' | 'path' | 'requestBody' | 'parameters' | 'responses'
> & {
  requestBody?: Record<string, any>
  parameters?: Record<string, RouteParameter> | RouteParameter[]
  responses?: {
    [statusCode: string]: RouteResponse
  }
}

Is that something that can be supported?

raiseUnknownParameters raises error when query starts with sub delimiter

The router currently returns a "400 Bad Request" error when:

  • raiseUnknownParameters is set to true in the router configuration
  • the query parameters start with a & (while it is unorthodox, this is allowed by RFC 3986)

Here an example based on the tutorial:
http://server/api/list?&page=1&isCompleted=true
which yields

"errors":{"":"is an unknown parameter"}

I would have thought that no such error would be raised in this case, since there is no unknown parameter (i.e. none).

Custom request body validation

Is there a way to make use of the zod Schema.strict in the automatic validation of a request body? Or override/disable the automatic validation?

At the moment if I create a schema

const TestSchema = z.object({
    foo: z.string()
})


class TestPost extends OpenAPIRoute {
    static schema = {
        requestBody: TestSchema,
    }
    ...
}

And then send this payload to the route

{
    "foo": "example",
    "bar": "example"
}

this will be accepted as valid even though "bar" is not a valid property on TestSchema
TestSchema.strict(...) is how I would handle this manually but unsure how/if the default validation behaviour would be modified. Or is the only way to handle this to parse the data again in the handler?

Implement Content Negotiation to Support Multiple Response Formats

To accommodate diverse client preferences, I propose introducing content negotiation. This feature allows clients to request their preferred content format (e.g., JSON, XML, HTML) via the Accept header, enabling the API to automatically adapt and serve the optimal media type.

Type error when using middleware

There is a type error when using ordinary itty-router middleware together with an OpenAPIRoute class instance:

import { withParams } from 'itty-router';
...
router.get('/devices/:id/alarms', withParams , TodoList);

The error reads:

TS2769: No overload matches this call.   Overload 1 of 3, '(path: string, ...handlers: OpenAPIRouteSchema[]): OpenAPIRouterSchema', gave the following error.     Argument of type '(request: IRequest) => void' is not assignable to parameter of type 'OpenAPIRouteSchema'.   Overload 2 of 3, '(path: string, ...handlers: RouteHandler<IRequest, []>[]): RouterType<Route, any[]>', gave the following error.     Argument of type 'typeof UnitDeviceAlarms' is not assignable to parameter of type 'RouteHandler<IRequest, []>'.       Type 'typeof UnitDeviceAlarms' provides no match for the signature '(request: IRequest): any'.

Note that the route and the middleware works as it should. This is just a type issue.

Allow custom Content-Types as responses

As OpenAPI Specification described here, the content type of the response is customizable.

for (const [key, value] of Object.entries(schema.responses)) {
const resp = new Resp(value.schema, {
description: value.description,
})
responses[key] = resp.getValue()
}
}

export class Resp extends Parameter {
constructor(rawType: any, params: ParameterLocation) {
// @ts-ignore
super(null, rawType, params)
}
// @ts-ignore
getValue() {
const value = super.getValue()
const contentType = this.params?.contentType ? 'this.params?.contentType' : 'application/json'
const param: Record<string, any> = {
description: this.params.description || 'Successful Response',
content: {},
}
param.content[contentType] = { schema: value.schema }
return param
}
}

But there is nowhere in the code to define the Content-Type of the response, so the generated document will always be application/json.

Doc pages breaking with Zod's bigint type

Using the bigint type of Zod seems to be breaking the /docs and redocs pages. A simple example based on the user guide, modified to use Zod's bigint instead of the normal Int:

import { OpenAPIRouter, OpenAPIRoute, Path, Int } from '@cloudflare/itty-router-openapi';
import { z } from 'zod'

export class ToDoFetch extends OpenAPIRoute {
	static schema = {
		parameters: {
			todoId: Path(z.coerce.bigint().nonnegative(), { // <-- using `bigint` here
				description: 'ToDo ID',
			}),
		},
	}

	async handle(
		request: Request,
		env: any,
		context: any,
		data: any
	) {
		const { todoId } = data.params
		return new Response(todoId);
	}
}

const router = OpenAPIRouter()
router.get('/todos/:todoId', ToDoFetch)

export default {
	fetch: router.handle,
}

The docs page then throws: "Failed to load API definition. Fetch error response status is 500 /openapi.json"
The redocs page is stuck in "Loading..."

When using the standard Int type instead, the doc pages load normally.

Add support for `nullable` at least in request body params

I've found myself needing a request body param to be either a string or null in a PATCH request. There's a required option, which means the param can be omited, but what I need is a explicit way to set a value as null, specially because I'm using PATCH to do partial updates or records in the database.

So far I've been using empty strings as a way to indicate that, but it's not ideal. It's confusing for the users of my API.

Thanks!

Nested router "No operations defined in spec"

Same as #30

Have tried setting up nested router (as I often do in itty-router) and while this works, I still see "No operations defined in spec!" when viewing /docs.

I've tested this on both my own projects and with the tasks example from Cloudflare Workers SDK which is at https://github.com/jasiqli/tasks-openapi-issue


Related to this, the example 7. Nested Routers shows

router.all('/api/v1/attacks/*', attacksRouter)

however I find this generates a "c is not function" error and the only way it will work (as with itty-router) is

router.all('/api/v1/attacks/*', attacksRouter.handle)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.