how to setup rpc/entrypoints with workers for API access?

hi. so i have an api that is structured as below, and i was wondering how i would represent it within rpc or entrypoints. do i create a base entrypoint and make the rest RPC targets that can be used with a getter (option 1, doesn't seem too difficult), or can i create it with option 2 using classes on the base api class?
GET /reports
GET /reports/:id
POST /reports
GET /communities
GET /communities/:id
GET /reports
GET /reports/:id
POST /reports
GET /communities
GET /communities/:id
// option 1
const reportsRoute = await env.BACKEND.getReports()
const reports = await reportsRoute.getReport("id")

// option 2
const reports = await env.BACKEND.reports.getReport("id")
// option 1
const reportsRoute = await env.BACKEND.getReports()
const reports = await reportsRoute.getReport("id")

// option 2
const reports = await env.BACKEND.reports.getReport("id")
1 Reply
oof2win2
oof2win27mo ago
like i want to go the second option but can i? referring to my original point here, i want pretty much this - but can i achieve that?
import type { CustomEnv } from "./types";
import { Kysely } from "kysely";
import { WorkerEntrypoint } from "cloudflare:workers";
import { SerializePlugin } from "kysely-plugin-serialize";
import type { DB } from "./db-types";
import { D1Dialect } from "./kysely-d1";

export class Categories {
constructor(protected env: CustomEnv) {}
/**
* Get a single category by its ID
* @param id ID of the category
*/
async getCategory(id: string) {
const category = await this.env.DB.selectFrom("Categories")
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
return category ?? null;
}

/**
* Get all categories
*/
async getAllCategories() {
const categories = await this.env.DB.selectFrom("Categories")
.selectAll()
.execute();
return categories;
}
}

export class FDGLBackend extends WorkerEntrypoint<Env> {
categories: Categories;
constructor(ctx: ExecutionContext, env: Env) {
super(ctx, env);
const customEnv: CustomEnv = {
...env,
d1_db: env.DB,
DB: new Kysely<DB>({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new SerializePlugin()],
}),
};
this.categories = new Categories(customEnv);
}

// required or wrangler crashes
async fetch() {
return new Response("ok");
}
}
import type { CustomEnv } from "./types";
import { Kysely } from "kysely";
import { WorkerEntrypoint } from "cloudflare:workers";
import { SerializePlugin } from "kysely-plugin-serialize";
import type { DB } from "./db-types";
import { D1Dialect } from "./kysely-d1";

export class Categories {
constructor(protected env: CustomEnv) {}
/**
* Get a single category by its ID
* @param id ID of the category
*/
async getCategory(id: string) {
const category = await this.env.DB.selectFrom("Categories")
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
return category ?? null;
}

/**
* Get all categories
*/
async getAllCategories() {
const categories = await this.env.DB.selectFrom("Categories")
.selectAll()
.execute();
return categories;
}
}

export class FDGLBackend extends WorkerEntrypoint<Env> {
categories: Categories;
constructor(ctx: ExecutionContext, env: Env) {
super(ctx, env);
const customEnv: CustomEnv = {
...env,
d1_db: env.DB,
DB: new Kysely<DB>({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new SerializePlugin()],
}),
};
this.categories = new Categories(customEnv);
}

// required or wrangler crashes
async fetch() {
return new Response("ok");
}
}
from what i see on the docs i need to change categories to a getter instead using the approach above, i got the error that categories is not a method on FDGLBackend (even when using a getter instead) okay so i have a cursed but functional setup
export class FDGLService extends WorkerEntrypoint<Env> {
#categories: Categories;
#communities: Communities;
#reports: Reports;
constructor(ctx: ExecutionContext, env: Env) {
super(ctx, env);
const customEnv: CustomEnv = {
...env,
d1_db: env.DB,
DB: new Kysely<DB>({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new SerializePlugin()],
}),
};
this.#categories = new Categories(customEnv);
this.#communities = new Communities(customEnv);
this.#reports = new Reports(customEnv);
}

get categories() {
return {
getCategory: this.#categories.getCategory.bind(this.#categories),
getAllCategories: this.#categories.getAllCategories.bind(
this.#categories,
),
health: this.#categories.health.bind(this.#categories),
};
}
}
export class FDGLService extends WorkerEntrypoint<Env> {
#categories: Categories;
#communities: Communities;
#reports: Reports;
constructor(ctx: ExecutionContext, env: Env) {
super(ctx, env);
const customEnv: CustomEnv = {
...env,
d1_db: env.DB,
DB: new Kysely<DB>({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new SerializePlugin()],
}),
};
this.#categories = new Categories(customEnv);
this.#communities = new Communities(customEnv);
this.#reports = new Reports(customEnv);
}

get categories() {
return {
getCategory: this.#categories.getCategory.bind(this.#categories),
getAllCategories: this.#categories.getAllCategories.bind(
this.#categories,
),
health: this.#categories.health.bind(this.#categories),
};
}
}
still wondering if there is a better solution so i figured this out using satisfies and a picker of the types within the class
type PickMatching<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
// biome-ignore lint/complexity/noBannedTypes: it's okay here as it is only a picker
type ExtractMethods<T> = PickMatching<T, Function>;

export class FDGLService extends WorkerEntrypoint<Env> {
#reports: Reports;
constructor(ctx: ExecutionContext, env: Env) {
super(ctx, env);
const customEnv: CustomEnv = {
...env,
d1_db: env.DB,
DB: new Kysely<DB>({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new SerializePlugin()],
}),
};
this.#reports = new Reports(customEnv);
}

get reports() {
// biome-ignore format: its nicer in one line
return {
getReport: this.#reports.getReport.bind(this.#reports),
getReports: this.#reports.getReports.bind(this.#reports),
createReport: this.#reports.createReport.bind(this.#reports),
revokeReport: this.#reports.revokeReport.bind(this.#reports)
} satisfies ExtractMethods<Reports>
}
}
type PickMatching<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
// biome-ignore lint/complexity/noBannedTypes: it's okay here as it is only a picker
type ExtractMethods<T> = PickMatching<T, Function>;

export class FDGLService extends WorkerEntrypoint<Env> {
#reports: Reports;
constructor(ctx: ExecutionContext, env: Env) {
super(ctx, env);
const customEnv: CustomEnv = {
...env,
d1_db: env.DB,
DB: new Kysely<DB>({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new SerializePlugin()],
}),
};
this.#reports = new Reports(customEnv);
}

get reports() {
// biome-ignore format: its nicer in one line
return {
getReport: this.#reports.getReport.bind(this.#reports),
getReports: this.#reports.getReports.bind(this.#reports),
createReport: this.#reports.createReport.bind(this.#reports),
revokeReport: this.#reports.revokeReport.bind(this.#reports)
} satisfies ExtractMethods<Reports>
}
}
its horrendous but i love it the annoying thing is that it doesn't export JSDoc comments as well
Want results from more Discord servers?
Add your server