Overview

An entrypoint is just a HTTP route that you can define. RΞASON uses file-based routing that is heavily inspired by Next.js. This means that to create a new entrypoint (route) you just create a new file inside the entrypoints/ directory.

For instance, to create a new route at POST /test, just create a file called test inside of the entrypoints directory and export a function called POST():

src/entrypoints/test.ts
export function POST(req: Request) {
  return "why don't scientists trust atoms? because they make up everything"
}

When you call this entrypoint, the response will be:

Response from `POST /test`

Using other methods

You can also use other HTTP methods by exporting named functions such as GET(), PUT(), DELETE() and PATCH():

src/entrypoints/test.ts

// POST /test
export function POST(req: Request) {
  return "why don't scientists trust atoms? because they make up everything"
}

// GET /test
export function GET(req: Request) {
  return "why don't scientists trust atoms? because they make up everything"
}

// PUT /test
export function PUT(req: Request) {
  return "why don't scientists trust atoms? because they make up everything"
}

// PATCH /test
export function PATCH(req: Request) {
  return "why don't scientists trust atoms? because they make up everything"
}

// DELETE /test
export function DELETE(req: Request) {
  return "why don't scientists trust atoms? because they make up everything"
}

Request and Response

RΞASON uses the Fetch API web standards for its request and response objects. If you not familiar with them, you quickly check their MDN documentation.

request

The request object is always passed as the first paremeter to your entrypoint function — export function POST(req: Request) {}.

With it, you can get information about the client’s request:

export async function POST(req: Request) {
  const body = await req.json()
  
  const url = req.url

  const fooHeader = req.headers.get('foo')
}

As said above, the req stricly follows the Web Standards — which means if you ever have any questions about it (on how to get the query params, for example) you can directly search on MDN.

response

As for the response object, you can just return a new Response() from your entrypoint, like:

src/entrypoints/test.ts
export async function POST(req: Request) {
  return new Response('Not authorized', { status: 401 })
}

If we call POST /test this is now the response:

Response from `POST /test` with a custom response

However RΞASON does not require that your entrypoint returns a Response object, you can return almost anything — more on this below.

Again, the response stricly follows the Web Standards — which means if you ever have any questions about it (on how to return a custom header) you can directly search on MDN.

Types of entrypoints

There are two types of entrypoints:

  1. Streaming entrypoints:
    • export function* POST() {} or export async function* POST() {}.
  1. Non-streaming entrypoints:
    • export function POST() {} or export async function POST() {}.

A streaming entrypoint always return a HTTP stream back to the client, allowing you to stream to the client. As for the non-streaming entrypoint, they cannot stream data, you can only return data once.

Although the above is the major distiction between the two, there is one more difference: you cannot return a custom response in a streaming entrypoint — i.e. return new Response().

So, what can you actually return in an entrypoint?

Return values

As mentioned above, what can an entrypoint return depends on its type. Let’s first start with non-streaming entrypoints.

Return values of non-streaming entrypoints

The following is what a non-streaming entrypoint can return:

  • Any JSON-serializable data — basically any piece of data that running JSON.parse() in it would work.
  • A custom Response with return new Response().

How to stream data in a steaming entrypoint

Before talking about what you can return in a streaming entrypoint, you need to first understand how can you stream data.

RΞASON try hard to use native JS/TS features, and in the context of streaming, this is also true: we use the native yield keyword.

Streaming example
async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export async function* GET() {
  yield 'Hello'
  await sleep(1000)

  yield ' World'
  await sleep(1000)

  yield '! ! !'
}

If you call this entrypoint:

The response

Some of you may not be familiar with the yield keyword and that’s ok! It is part of the generator feature set of JavaScript and we’ll go in-depth about them in the next page. Don’t worry for now.

You can, of course, also use the return keyword:

Streaming with return example
async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export async function* GET() {
  yield 'Hello'
  await sleep(1000)

  yield ' World'
  await sleep(1000)

  yield '! ! !'
  await sleep(1000)

  return 'foo bar'
}

Calling this entrypoint now:

The response with `return`

So why use yield instead of return? Because when you use return in a function, that function immediatly stops executing — meaning you will not be able to stream more values after you call return.

Cool! Now we can go over the types of values you can yield/return in a streaming entrypoint.

Return values of streaming entrypoints

The following is what a streaming entrypoint can yield:

  • Strings;
  • Objects;

Once you yield a certain type you cannot yield the other type in that entrypoint. For instance, the following code will error:

export async function* GET() {
  yield 'Hello'
  yield { value: 'World! ! !' }
}

And this is what a streaming entrypoint can return:

  • String;
  • Object;
  • Generator;

Again, you can only return the type you previously yielded. You cannot yield an object and then return a string. If you haven’t yielded anything, then you are free to return whatever you want.

There are major differences between the three types. Let’s go over them now.

Returning/yielding objects

By far, the one you will be yielding/returning the most is object. It is extremely important that you understand how it works.

By default, HTTP streams only supports text. So how does RΞASON allows you to stream back objects? That’s because we apply a special encoding to your data before sending it. This is key to understand.

So, what encoding does RΞASON apply? A custom one.

Using an encoding means that your client needs to decode your data before being able to read it. We know this is a cost — both in terms of developer ergonomic & maintainability.

Given the need to decode data in your client, RΞASON has a library that allows for easy decoding: a React library and a general JavaScript library for any enviroment that runs JS/TS.

Although recommended, you’re not forced to use RΞASON’s encoding: there’s a way to return value that makes RΞASON do not apply any encoding to it that we talk about below.

The important thing to know is: in your backend RΞASON app, you can return objects ({ foo: 'bar', baaz: true }) and in your frontend (client) you will receive the JSON object the backend streamed back:

Streaming objects example
async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export async function* GET() {
  let obj = {
    foo: 'bar'
  }
  yield obj
  await sleep(1000)

  obj.foo += '!!!'
  yield obj
  await sleep(1000)

  obj.baaz = true
  yield obj
  await sleep(1000)

  obj.array = [1]
  yield obj
  await sleep(1000)
  
  obj.array.push(2, 3)
  yield obj
}

Calling the entrypoint in RΞASON Playground will return:

The streaming response

In the example above we were always just yielding obj — that is not needed though. Here’s an equivalent code that’ll produce the same output:

Streaming objects example
async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export async function* GET() {
  yield {
    foo: 'bar'
  }
  await sleep(1000)

  yield {
    foo: 'bar!!!'
  }
  await sleep(1000)

  yield {
    baaz: true
  }
  await sleep(1000)

  yield {
    array: [1]
  }
  await sleep(1000)
  
  yield {
    array: [1, 2, 3]
  }
}

Calling the entrypoint in RΞASON Playground will result in the same output:

The streaming response

This highlights an important aspect of streaming data in RΞASON: you can return the object you want to & RΞASON will take care of streaming it to you in the most efficient way possible.

For instance:

yield {
  foo: 'bar'
}
// this will stream the `bar` string to the client

yield {
  foo: 'bar!!!'
}
// this will only stream the `!!!` string to the client
// that's because only the `!!!` is new information.

This logic also applies to nested objects:

Streaming nested objects example
async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export async function* GET() {
  yield {
    foo: {
      bar: 'BAR'
    }
  }
  await sleep(1000)

  yield {
    foo: {
      baaz: 'BAAZ'
    }
  }
}

Calling the entrypoint in RΞASON Playground will result in:

The streaming response

Returning/yielding a string

Returning strings is an advanced usecase and that’s because RΞASON will not apply any sort of encoding to it. That means this an escape-hatch when you are you don’t want RΞASON to encode your data.

There are no restrictions when yielding/retuning strings. You are free to do whatever (as long as you return/yield string).

Just remember that you can only return/yield either strings or objects. Never both in the same entrypoint.

Returning a generator

You probably will never need to consciously return a generator. Why does this exists then?

Because reasonStream is an async generator, and we want to be able to just return it for ease-of-use:

Returning a generator
import { reasonStream } from 'tryreason'

export async function* GET() {
  return reasonStream('tell me a joke')
}

The streaming response

When you return a generator (async or not), RΞASON will run it and stream back its yielded values back to the client following the same rules you would find in a normal streaming endpoint.

Which means that, if your generator yields an object, it’ll be encoded and if it yields a string it’ll be streamed with not encoding.


Next steps

Following next, we’ll learn about one of the building blocks of RΞASON: the reason() function!