Entrypoints
An entrypoint is an endpoint an user can request
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()
:
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()
:
// 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:
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:
- Streaming entrypoints:
export function* POST() {}
orexport async function* POST() {}
.
- Non-streaming entrypoints:
export function POST() {}
orexport 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
withreturn 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.
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:
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:
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:
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:
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:
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!
Was this page helpful?