Routing within a single endpoint
Gracile doesn’t aim to replace full-fledged HTTP frameworks like Express, Hono,
Elysia, Fastify, etc.
However, you might want to colocate JSON endpoints with your front-end, to
achieve what is often called the “Back-For-Front” pattern.
It’s when you want to put a thin bridge whose role is to authenticate with and
curate your “real” backends.
That being said, Gracile is full-stack but is leaning more on the front-end
side. This is perfect for the typical scenario where “serverless” is chosen (aka
more or less custom JS runtime running near the user).
But it is also a good practice to not put too much processing weight on the
rendering side of your app/site, whether it is server-rendered upfront, or
not.
Single entry point API routes can also be used to set up OpenAPI driven
handlers, GraphQL, tRPC…
But we will stick with pure JSON here.
Note that when possible, it’s clearer to keep a file-based routing, like
/api/pet/[id].ts, but sometimes it’s more maintainable to use programmatic
routing in a single file.
For that, we can use a [...rest].ts “catch-all” route and process the URL
forward ourselves.
Pre-requisites
In this recipe, we will explore the URLPattern API to help us with
routing.
At the time this guide is written,
isn’t supported widely, whether on
server runtimes or browsers.
Fortunately, there is a polyfill available, which is the one used under the hood by Gracile already to achieve file-based routing.
npm i urlpattern-polyfill
Files
Here is a very contrived example, from where you can elaborate with POST, more
URLPattern, hand-shakes and data sourcing from/to your back-ends…
Defining more route patterns for your single-file API entry point will start to
make sense over just using the file-based routing. (Like already said above,
/api/pet/:id/ could already be defined in the Gracile file router with
/api/pet/[id].ts).
But we just want to demonstrate the basic primitives for now:
📄 /src/routes/api/[...path].ts
import { defineRouteimport defineRoute } from '@gracile/gracile/route';
Usually defined as a global in the browser.
import { URLPatternclass URLPattern } from 'urlpattern-polyfill';
const petDbconst petDb: {
id: number;
name: string;
type: string;
}[]
= [
{ idid: number : 1, namename: string : 'Rantanplan', typetype: string : 'dog' },
{ idid: number : 1, namename: string : 'Felix', typetype: string : 'cat' },
];
const petStorePatternconst petStorePattern: URLPattern = new URLPatternnew URLPattern(init?: URLPatternInput, baseURL?: string): URLPattern (
'/api/pet/:id/',
'http://localhost:9898/',
);
export default defineRouteimport defineRoute ({
handlerhandler: (context: any) => Response : (contextcontext: any ) => {
if (contextcontext: any .request.method !== 'GET')
return Responsevar Response: {
new (body?: BodyInit | null, init?: ResponseInit): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit): Response;
redirect(url: string | URL, status?: number): Response;
}
The Response interface of the Fetch API represents the response to a request.
.jsonfunction json(data: any, init?: ResponseInit): ResponseThe json() static method of the Response interface returns a Response that contains the provided JSON data as body, and a Content-Type header which is set to application/json. The response status, status message, and additional headers can also be set.
(
{ successsuccess: boolean : false, messagemessage: string : `Only "GET" is allowed.` },
{ statusResponseInit.status?: number | undefined : 405 },
);
const resultconst result: URLPatternResult | null = petStorePatternconst petStorePattern: URLPattern .execURLPattern.exec(input?: URLPatternInput, baseURL?: string): URLPatternResult | null (contextcontext: any .url);
const idconst id: string | undefined = resultconst result: URLPatternResult | null ?.pathnameURLPatternResult.pathname: URLPatternComponentResult .groupsURLPatternComponentResult.groups: {
[key: string]: string | undefined;
} | undefined
?.idstring | undefined ;
if (!Numbervar Number: NumberConstructorAn object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
.isNaNNumberConstructor.isNaN(number: unknown): booleanReturns a Boolean value that indicates whether a value is the reserved value NaN (not a
number). Unlike the global isNaN(), Number.isNaN() doesn't forcefully convert the parameter
to a number. Only values of the type number, that are also NaN, result in true.
(idconst id: string | undefined )) {
const foundPetconst foundPet: {
id: number;
name: string;
type: string;
} | undefined
= petDbconst petDb: {
id: number;
name: string;
type: string;
}[]
.findArray<{ id: number; name: string; type: string; }>.find(predicate: (value: {
id: number;
name: string;
type: string;
}, index: number, obj: {
id: number;
name: string;
type: string;
}[]) => unknown, thisArg?: any): {
id: number;
name: string;
type: string;
} | undefined (+1 overload)
Returns the value of the first element in the array where predicate is true, and undefined
otherwise.
((petpet: {
id: number;
name: string;
type: string;
}
) => petpet: {
id: number;
name: string;
type: string;
}
.idid: number === Numbervar Number: NumberConstructor
(value?: any) => number
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
(idconst id: string | undefined ));
if (foundPetconst foundPet: {
id: number;
name: string;
type: string;
} | undefined
) return Responsevar Response: {
new (body?: BodyInit | null, init?: ResponseInit): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit): Response;
redirect(url: string | URL, status?: number): Response;
}
The Response interface of the Fetch API represents the response to a request.
.jsonfunction json(data: any, init?: ResponseInit): ResponseThe json() static method of the Response interface returns a Response that contains the provided JSON data as body, and a Content-Type header which is set to application/json. The response status, status message, and additional headers can also be set.
({ successsuccess: boolean : true, datadata: {
id: number;
name: string;
type: string;
}
: foundPetconst foundPet: {
id: number;
name: string;
type: string;
}
});
return Responsevar Response: {
new (body?: BodyInit | null, init?: ResponseInit): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit): Response;
redirect(url: string | URL, status?: number): Response;
}
The Response interface of the Fetch API represents the response to a request.
.jsonfunction json(data: any, init?: ResponseInit): ResponseThe json() static method of the Response interface returns a Response that contains the provided JSON data as body, and a Content-Type header which is set to application/json. The response status, status message, and additional headers can also be set.
(
{ successsuccess: boolean : false, messagemessage: string : `Pet "${idconst id: string | undefined }" not found!` },
{ statusResponseInit.status?: number | undefined : 404 },
);
}
return Responsevar Response: {
new (body?: BodyInit | null, init?: ResponseInit): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit): Response;
redirect(url: string | URL, status?: number): Response;
}
The Response interface of the Fetch API represents the response to a request.
.jsonfunction json(data: any, init?: ResponseInit): ResponseThe json() static method of the Response interface returns a Response that contains the provided JSON data as body, and a Content-Type header which is set to application/json. The response status, status message, and additional headers can also be set.
(
{ successsuccess: boolean : false, messagemessage: string : `Unknown API route for "${contextcontext: any .url}"` },
{ statusResponseInit.status?: number | undefined : 400 },
);
},
});
📄 /src/routes/my-api-consumer.ts
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { defineRouteimport defineRoute } from '@gracile/gracile/route';
import { documentconst document: () => any } from '../document.js';
export default defineRouteimport defineRoute ({
documentdocument: (context: any) => any : (contextcontext: any ) => documentfunction document(): any (contextcontext: any ),
templatetemplate: () => TemplateResult<1> : () => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<script type="module">
const pet = await fetch('/api/pet/1/').then((r) => r.json());
const noPet = await fetch('/api/pet/10/').then((r) => r.json());
const wrongMethod = await fetch('/api/pet/1/', {
method: 'DELETE',
}).then((r) => r.json());
console.log({ pet, noPet, wrongMethod });
</script>
<h1>My JSON API consumer</h1>
`,
});