API

Vitedge can optionally create API endpoints. The advantage of this is having frontend and backend running in the same domain to prevent cross-origin issues and preflight requests.

Rest

Routes

Every file in <root>/functions/api will be accessible from th API.

For example, <root>/functions/api/my/function.js (or *.ts) will be built as /api/my/function endpoint and can be requested from the frontend.

Versioning

The API can be organized in subfolders such as <root>/functions/api/v1/* to provide API versioning.

Dynamic routes

Dynamic routes (e.g. /api/users/:id) can be specified by using brackets in file or directory names as follows:

  • Required parameter: Single brackets <root>/functions/api/users/[id].js. This will match /api/users/xxx and provide { params: { id: 'xxx' } } to the handler.
  • Optional parameter: Double brackets <root>/functions/api/users/[[id]].js. This will match the previous example in the same way, but also /api/users and provide { params: undefined } to the handler.
  • Catch all: Brackets with ... prefix <root>/functions/api/users/[...all].js. This will match /api/users/deep/value and provide { params: { all: 'deep/value' } } to the handler. Note that this won't match a missing parameter (/api/users).

Directories can also follow the same naming convention to have more than 1 parameter: <root>/functions/api/[:directory]/[:file].js turns into { params: { directory: '...', file: '...' } }.

Handlers

Each handler file looks like this:

export default {
  async handler({ event, request, params }) {
    if (request.method !== 'GET') {
      throw new Error('Method not supported!')
    }

    // Optional:
    // return new Response('...', { headers: { ... } })

    return {
      // This will be treated as JSON
      data: { msg: 'Hello world!' },
      headers: {} // Optional dynamic headers
      status: 200 // Optional status (200 is default)
      cache: {} // Optional dynamic cache
    }
  },
  options: {
    cache: {
      api: 85, // Cache's max-age in seconds
    },
    // Static optional headers
    headers: {
      'content-type': 'application/json', // This is default
    },
  },
}

The actual handler gets the event (FetchEvent) and request (fetch Request) objects provided by the running platform. Note that some properties of these objects might be missing during development (such as CF's location information).

The response must be either a new fetch Response or an object with data property that contains a stringifiable object or plan text.

Note on headers: Use always lower case for header keys.

Errors

Any error thrown in the handler will translate to a JSON payload containing the information of this error:

{
  error: {
    status: 500,
    message: 'yikes',
    details: { /* ... */ },
    stack: 'Available only during development'
  }
}

Built-in errors

Vitedge provides a handy group of built-in errors that represent different status codes. Check the import types for more.

import {
  MethodNotAllowedError,
  ForbiddenError,
  UnknownError,
} from 'vitedge/errors.js'

// ...

throw new MethodNotAllowedError('Only GET is allowed', {
  /* details */
})

You can also extend the errors to create your own:

import { RestError } from 'vitedge/errors'

export class ImATeapotError extends RestError {
  constructor(message, details) {
    super(message, 418, details)
  }
}

throw new ImATeapotError('Yolo')

Types

In order to get type validation and autocompletion, do one of the following:

// Only TypeScript
import { ApiEndpoint } from 'vitedge'

export default <ApiEndpoint>{
  // handler, options, ...
} // as ApiEndpoint // <- This is equivalent
// JavaScript or TypeScript
import { defineApiEndpoint } from 'vitedge/define'

export default defineApiEndpoint({
  // handler, options, ...
})

Consuming the API

You can freely call your API from the frontend. In order to make the API available in from other domains, configure CORS in the worker/node entry point.

Generally, it is not possible to make self-requests when running on CF Workers (e.g. calling your own API during SSR, from worker to worker). However, Vitedge will automatically redirect these API requests to local function calls during SSR. Simply make sure you use fetch or any library that uses it internally.

export default {
  name: 'MyPage',
  async setup() {
    // In Browser rendering, this behaves as a normal fetch.
    // In SSR, it directly calls the corresponding API handler.
    const response = await fetch('/api/hello/world')
    const data = await response.json()

    return { data }
  },
}

Other endpoints

Apart from <root>/functions/api/**/* directory, Vitedge will consider any file directly under <root>/functions/* to be similar to an API endpoint (following similar syntax in the handlers and cache options).

Thanks to this, it can support other use cases such as the following.

GraphQL

Add <root>/functions/graphql.js and setup a GraphQL server using apollo-server-cloudflare or any other tool compatible with your deployment platform.

Dynamic sitemap, robots, and other files

Add <root>/functions/sitemap.js or <root>/functions/robots.js and return the corresponding content with cache options. See example here. Both /sitemap.xml and sitemap.txt will match and run the handler (check the request's URL if you want to return different values for each extension).

Note that static files have higher priority than dynamic files. For example, if you have both <root>/public/sitemap.xml and <root>/functions/sitemap.js, the former will be served.