netzo / fresh-netzo

Full-stack Deno Fresh meta-framework for building business web apps like internal tools, dashboards, admin panels and automated workflows.
https://netzo.io
MIT License
51 stars 3 forks source link

[resources] move resource declaration into separate file #118

Closed miguelrk closed 9 months ago

miguelrk commented 9 months ago

TLDR: move resource declaration into separate file e.g. resources/<resource>.ts as a best-practice instead of inlining everything in the entrypoint file.

Context

The current registration of resources is minimal and can easily be inlined, serving also as a great overview:

export const netzo = await Netzo({
  api: {
    path: "/api",
    idField: "id",
    endpoints: {
      accounts: DenoKvResource({ kv, prefix: ["accounts"] }),
    },
  },
});

if (import.meta.main) netzo.start();

However, once resources become feature complete with e.g. hooks and resolvers, the resource options will grow and this will no longer be the case, instead, it might grow to something like this:

export const netzo = await Netzo({
  api: {
    path: "/api",
    idField: "id",
    endpoints: {
      accounts: {
        resource: DenoKvResource({ kv, prefix: ["accounts"] }),
        hooks: {...},
      },
    },
  },
});

if (import.meta.main) netzo.start();

with the hooks object "exploding" considerably. And this is only for a single resource...

Alternatives

We consider the following alternatives, from 1, the most "implicit" or config-first approach (netzo handles registering routes and middleware), to 3, the most "explicit" or code-first approach (the user handles registering routes and middleware with netzo helpers).

1. Keep declaration in entrypoint file (current approach)

See context above, will not scale well.

2. Move declaration into separate file e.g. resources/<resource>.ts

Scales better than 1 and remains config-first (netzo can still handle the heavy lifting of registering routes and middleware).

// netzo.ts
import { accounts } from "@/resources/accounts.ts";

export const netzo = await Netzo({
  api: {
    path: "/api",
    idField: "id",
    endpoints: {
      accounts,
    },
  },
});
// resources/accounts.ts
export const accounts = defineResource({
  resource: DenoKvResource({ kv, prefix: ["accounts"] }),
  hooks: {...},
});

3. Replace plugin with manual API route registration

This would be the most explicit and align best with fresh, however, it also requires the most setup and duplicate code. Netzo can still provide helpers to make this easier and do some heavy lifting, but it would still be much more code for the user to have to handle. It could look something like the following:

// routes/api/accounts/index.ts
export const handler = createApiRoutesHandler({
  resource: DenoKvResource({ kv, prefix: ["accounts"] }),
  hooks: {...},
});

// routes/api/accounts/[id].ts
export const handler = createApiRoutesHandler({
  resource: DenoKvResource({ kv, prefix: ["accounts"] }),
  hooks: {...},
});

// routes/api/accounts/_middleware.ts
export const handler = createApiMiddlewareHandler({
  resource: DenoKvResource({ kv, prefix: ["accounts"] }),
  hooks: {...},
});

Conclusion

It seems that 3 looses a lot in DX in comparison with 2, since fresh does not allow registering all routes and middleware from a single file (at least requiring an index.ts, [id].ts and a _middleware.ts). Also, thinking about it, the API endpoints provided by the API module should not be customizable for the most part, on the contrary, the goal of the API module is maximize speed and enforce best practices and conventions by stricly adhering to RESTful (resource-oriented) patterns. Users are still free to add extra routes/middleware under the routes/ folder to complement these resource endpoints which are auto-injected by netzo (e.g. for doing partials). Due to the above, we will pursue 2 in https://github.com/netzo/netzo/pull/116 at least for now, this can and will most certainly evolve.

miguelrk commented 9 months ago

Closing as done with a mixed approach of moving declaration to @routes/api/<resource>.ts (server-only) and schemas, generators and utils to @/data/<resource>.ts. This since mixing both client- and server-side code was causing leaks and harming DX. Due to this, it was decided to move API endpoint declaration to @routes/api/<resource>.ts in favor of explicitness (matches route location where it will be mounted). The resources can still be inlined directly in netzo.ts, but this quickly grows, so we avoid doing so in the templates.

// routes/api/accounts.ts
export const accounts = defineApiEndpoint({
  resource: DenoKvResource({ prefix: ["accounts"] }),
  hooks: {...},
});