QwikDev / qwik

Instant-loading web apps, without effort
https://qwik.dev
MIT License
20.83k stars 1.31k forks source link

[✨] Expose `globalLoader$` and add new parameter to `globalAction$` #5815

Closed fprl closed 9 months ago

fprl commented 9 months ago

Is your feature request related to a problem?

I'm not sure if it's a problem, but since directory-based routing (DBR) isn't really flexible, it might come in handy. One issue I always face with DBR is that declaring routes and API endpoints gets messy as soon as you have more than 5 routes. Although this idea will not solve the issue with regular routes, it may solve API endpoints in a better way.

Describe the solution you'd like

Exposing a globalLoader$ for declaring global loaders and adding path parameter to globalAction$. I know that routeLoaders$ works fine for collocation; however, it's difficult to scale with this approach:

globalLoader$ and globalAction$ should a path parameter that registers the loader/action in a specific path provided by the user, this includes accepting dynamic route segments, so you don't even have to follow the DBR convention and their URL is not constructed based on where the file its located.

Current

For example, this is how you declare API endpoints now (index hell):

.
└── qwik-app-demo/
    └── src/
        └── routes/
            ├── api/
            │   ├── houses/
            │   │   ├── index.ts
            │   │   └── [id]/
            │   │       └── index.ts
            │   └── users/
            │       ├── index.ts
            │       └── [id]/
            │           ├── index.ts
            │           ├── activity/
            │           │   └── index.ts
            │           ├── change-password/
            │           │   └── index.ts
            │           └── settings/
            │               └── index.ts
            ├── v2/
            │   ├── houses/
            │   │   ├── index.ts
            │   │   └── [id]/
            │   │       └── index.ts
            │   └── users/
            │       ├── index.ts
            │       └── [id]/
            │           ├── index.ts
            │           ├── activity/
            │           │   └── index.ts
            │           ├── change-password/
            │           │   └── index.ts
            │           └── settings/
            │               └── index.ts
            ├── my-account/
            │   └── index.tsx
            ├── index.tsx
            ├── layout.tsx
            └── service-worker.ts

Proposed

How it may look. Please notice that, as they are not regular endpoints and can be declared in any file inside src directory like globalAction$, they don't need to follow DBR conventions like /path/index.ts.

.
└── qwik-app-demo/
    └── src/
        └── routes/
            ├── api/
            │   ├── houses.ts // loaders and actions for api/houses/*
            │   └── users.ts // loaders and actions for api/users/*
            │   ├── houses-v2.ts // loaders and actions for api/v2/houses/*
            │   └── users-v2.ts // loaders and actions for api/v2/users/*
            ├── my-account/
            │   └── index.tsx
            ├── index.tsx
            ├── layout.tsx
            └── service-worker.ts

Where src/routes/api/users.ts could look like:

// path is mandatory to construct URL path: `/api/users`
export const useGetUsers = globalLoader$('/api/users', async (req) => {
  ...
});

// accepts dynamic route segment: `/api/users/123`
export const useGetUser = globalLoader$('/api/users/:id', async (req) => {
  ...
});

export const useGetUserProfile = globalLoader$('/api/users/:id/profile', async (req) => {
  ...
});

export const useGetUserSettings = globalLoader$('/api/users/:id/settings', async (req) => {
  ...
});

// I'm dreaming but ideally you can also export a global action from here and it will behave the same way: 
// POST/PATCH/DELETE `/api/users/123`
export const useModifyUser = globalAction$(('/api/users/:id', data, event) => {
  // do something based on event.method
});

// POST/PATCH/DELETE `/api/users/123/change-password`
export const useUserChangePassword = globalAction$(('/api/users/:id/change-password', data, event) => {
  // do something based on event.method
});

Calling it from a page:

import { component$ } from "@builder.io/qwik";

import { useGetUserProfile } from '~/routes/api/users';

export default component$(() => {
  const profile = useProfile();

  return (
    ...
  )
});

TL;DR

Describe alternatives you've considered

  1. globalLoader$ and globalAction$ path parameter to construct their URL relative to the file where they are located, so you have to follow the DBR convention. For example: Where src/routes/api/users/index.ts could look like:
    
    import { globalLoader$ } from "@builder.io/qwik-city";

// path is mandatory to construct URL path: /api/users export const useUsers = globalLoader$('/', async (req) => { ... });


Where `src/routes/api/users/[id]/index.ts` could look like:
```ts
import { globalLoader$, globalAction$ } from "@builder.io/qwik-city";

// path is mandatory to construct URL path: `/api/users/123`
export const useGetUser = globalLoader$('/', async (req) => {
  ...
});

// I'm dreaming but ideally you can also export a global action from here and it will behave the same way:
// POST/PATCH/DELETE `/api/users/123`
export const useModifyUser = globalAction$(('/', data, event) => {
  // do something based on event.method
});

// `use` will be removed from the final path: `/api/users/123/profile`
export const useProfile = globalLoader$('/profile', async ({ params }) => {
  ...
});

// `use` will be removed from the final path: `/api/users/123/settings`
export const useProfile = globalLoader$('/settings', async ({ params }) => {
  ...
});
  1. Exposing an alternative to directory-based routing (DBR), but I'm not sure that will solve the issue as routeLoader$ has a better DX than a regular endpoint.

Additional context

globalAction$ already exists!

wmertens commented 9 months ago

I understand where you're coming from and I've also struggled with the API impact.

However, I think it's a bad idea to want to have an external API that the Qwik app also uses internally. You're making things harder for both use cases. Qwik-city is actually awesome because you don't need to worry about API, you can flexibly do what you want.

An external API however needs to be rigid and precise.

Those are two quite different goals, and therefore I think it's better to use another tool for the external API.

Personally, I think graphql is a phenomenal way to provide an external API.

But for a qwik-city app internally, just use the invisible API via loaders and actions instead.

fprl commented 9 months ago

Thanks for your answer @wmertens.

I agree that using other tool may be just better and easier, I could use the entry.fastify.tsx to handle all API endpoints and at the same time share all the types in the same codebase.

Will do that then, thanks a lot!