Blitz: Is it possible to support named export for API in the future?

Created on 21 Sep 2020  路  17Comments  路  Source: blitz-js/blitz

What do you want and why?

Clean up imports

Current

import getChannel from 'app/channels/queries/getChannel';
import getChannels from 'app/channels/queries/getChannels';
import createChannel from 'app/channels/mutation/createChannel';

Expected

import { getChannel, getChannels } from 'app/channels/queries';
import { createChannel } from 'app/channels/mutation';
// or even more 
import { getChannel, getChannels, createChannel } from 'app/channels';

Possible implementation(s)

-

Additional context

-

kinfeature-change needs investigation prioritlow statuassigned

Most helpful comment

I have been working on some stuff to make the API layer work with Expo and React Native. This almost necessarily involves a Babel plugin but also unlocks a lot of stuff. The import on the client side is transpiled to the RPC client in the same file. As such,

import getUser from 'app/queries/getUser';

to,

/** 
- The RPC client split out into a small module that wraps the fetch call and interprets 
the _meta tag from the resolver. 
- No node code should be on the import path since that makes it difficult to make the client
 versatile. Especially for Expo and React Native. 
- We can either try to nullify all node imports with the build step or for the client side we
 can just not import any of that code (since all we need is fetch and every one uses Babel).
- Can be customized by Babel plugin or blitz.config.js to include middleware imports and 
modify the client, eg. different DataStore and auth techniques
*/
import { getResolverRpcClient } from '@blitzjs/client';

const getUser = getResolverRpcClient(
    'app/_resolvers/queries/getUser',
    'getUser',
    'query'
)

But using the plugin we can easily manage named exports on all sides. The named exports can be considered as sub-functions, so part of the RPC call can be specifying which of the named exports to run. And since we import using import * as resolver from '....', we can using that parameter from the RPC call to determine which function to run (resolver[functionName]).

The call on the frontend can be compiled using the babel plugin:

import getUserData, { getUserTasks } from 'app/queries/getUserData';

to,

import { getResolverRpcClient } from '@blitzjs/client';

const getUserData = getResolverRpcClient(
    'app/_resolvers/queries/getUserData',
    'getUserData',
    'query'
)

const getUserTasks = getResolverRpcClient(
    'app/_resolvers/queries/getUserData',
    'getUserData.getUserTasks',
    'query'
)

I think this is important too, since Vercel has limits on the number of serverless functions you can define (I think somewhere around 20). So as the number of functions increase, it'll quickly reach that limit. And if we want to encourage modular server side functions as well, we should figure out a way to do this well.

I am new to this sorry, but I am really excited about this project! This is the first project that I am trying to get properly involved with. I see a lot of ways to play with this amazing DX and expand the domain that can use a blitz backend (even if then nextjs frontend is not always used, eg. Expo or other frameworks. NextJS can be used just for the serverless functions). Since the client is just a fetch call, it can be used with Svelte, Vue, etc. Any JS framework can have an api layer by defining functions next to the client side code which can compiled away by blitz and they are used as imports. I think since NextJS is already using Babel, we can use its powers to enable a lot more stuff.

This will also help with #794

@flybayer Would love to hear your thoughts!

All 17 comments

Hi, @xiaoyu-tamu thanks for the request. I deleted the previous comment because the info I provided was wrong. Sorry about that.

I'm sure we can figure out a way to make this work, but it could be complicated.

Anyone is welcome to dig in and try to figure it out.

@ryardley I'm also curious your thoughts on this.

This could be a neat idea but we would have to generate indexes in the sourcecode and would want to check it doesn't make certain automatic IDE refactorings break: ie. moving a mutation to another context. I have a feeling that might make it not really work very well. It could be easier to compile and inject the mutation/query via a client with typescript types in a similar way to the way prisma does assuming the goal here is to make using mutations/queries easier or source them from a single point

I have been working on some stuff to make the API layer work with Expo and React Native. This almost necessarily involves a Babel plugin but also unlocks a lot of stuff. The import on the client side is transpiled to the RPC client in the same file. As such,

import getUser from 'app/queries/getUser';

to,

/** 
- The RPC client split out into a small module that wraps the fetch call and interprets 
the _meta tag from the resolver. 
- No node code should be on the import path since that makes it difficult to make the client
 versatile. Especially for Expo and React Native. 
- We can either try to nullify all node imports with the build step or for the client side we
 can just not import any of that code (since all we need is fetch and every one uses Babel).
- Can be customized by Babel plugin or blitz.config.js to include middleware imports and 
modify the client, eg. different DataStore and auth techniques
*/
import { getResolverRpcClient } from '@blitzjs/client';

const getUser = getResolverRpcClient(
    'app/_resolvers/queries/getUser',
    'getUser',
    'query'
)

But using the plugin we can easily manage named exports on all sides. The named exports can be considered as sub-functions, so part of the RPC call can be specifying which of the named exports to run. And since we import using import * as resolver from '....', we can using that parameter from the RPC call to determine which function to run (resolver[functionName]).

The call on the frontend can be compiled using the babel plugin:

import getUserData, { getUserTasks } from 'app/queries/getUserData';

to,

import { getResolverRpcClient } from '@blitzjs/client';

const getUserData = getResolverRpcClient(
    'app/_resolvers/queries/getUserData',
    'getUserData',
    'query'
)

const getUserTasks = getResolverRpcClient(
    'app/_resolvers/queries/getUserData',
    'getUserData.getUserTasks',
    'query'
)

I think this is important too, since Vercel has limits on the number of serverless functions you can define (I think somewhere around 20). So as the number of functions increase, it'll quickly reach that limit. And if we want to encourage modular server side functions as well, we should figure out a way to do this well.

I am new to this sorry, but I am really excited about this project! This is the first project that I am trying to get properly involved with. I see a lot of ways to play with this amazing DX and expand the domain that can use a blitz backend (even if then nextjs frontend is not always used, eg. Expo or other frameworks. NextJS can be used just for the serverless functions). Since the client is just a fetch call, it can be used with Svelte, Vue, etc. Any JS framework can have an api layer by defining functions next to the client side code which can compiled away by blitz and they are used as imports. I think since NextJS is already using Babel, we can use its powers to enable a lot more stuff.

This will also help with #794

@flybayer Would love to hear your thoughts!

@nksaraf so sorry for the unusually long delay in replying! But that's fantastic work you've done! I love that type of stuff :) Using a babel plugin is definitely a great option.

That said, I don't we can support named exports like the OP wants, because we have a hard requirement of having the type imports work statically.

Also, I'm not super pumped about allowing each RPC endpoint to have more than one possible function. I think a better solution would be something like this which would allow keeping one function per endpoint:

const getUserTasks = getResolverRpcClient(
    'app/_resolvers/queries/getUserData/getUserTasks',
    'getUserTasks',
    'query'
)

I think this is important too, since Vercel has limits on the number of serverless functions you can define

This does not affect Next.js/Blitz apps. They automatically consolidate functions, so you don't have to think about it.

This does not affect Next.js/Blitz apps. They automatically consolidate functions, so you don't have to think about it.

I am sorry but I don't understand this? How does Next consolidate functions? Each query/mutation we define becomes a serverless function right? This is what I gathered from the build output and vercel build logs. I might be mistaken? On the free tier, Vercel has a limit of 12 serverless functions only. So this can cause limitations in terms of the number of queries/mutation we can define.

Reference below:
https://vercel.com/docs/platform/limits#general-limit-examples

That said, I don't we can support named exports like the OP wants, because we have a hard requirement of having the type imports work statically.

I completely agree with this and the type imports should work statically. OP's version might not work, but I think this can work:

import getUserData, { getUserTasks } from 'app/queries/getUserData';

This will maintain that static type-checking since these are the actual exports from that file as well. We might not want to collapse queries across files, but I think we can define multiple queries in one file (one default and multiple named exports) and use them safely on the frontend.

@nksaraf

If I'm not wrong, this limitation is not for next.js projects

NOTE: Next.js bundles all API Routes and server-rendered pages into individual Serverless Functions when deployed on Vercel. As a result, the "Serverless Functions per Deployment" limit is unlikely to apply for Next.js projects.

I read this too and was still unclear what it meant. It says it's "unlikely". Just to understand this.. if we have 'queries/getUser' and 'queries/getTasks' wouldn't this be two different serverless functions or are they bundled together somehow. This might not be the best place to answer this but this was my confusion.

The team at Vercel have confirmed multiple times that the function limit doesn't affect nextjs apps unless your app is super massive.

I quote from @timer from the Vercel slack:

Next API routes will let you have hundreds of unique endpoints without bumping into the 12 limit

That's because they automatically consolidate endpoints into shared lambdas.

That's because they automatically consolidate endpoints into shared lambdas.

Ohh thats great! That makes things simpler.

Also, I'm not super pumped about allowing each RPC endpoint to have more than one possible function. I think a better solution would be something like this which would allow keeping one function per endpoint:

const getUserTasks = getResolverRpcClient(
  'app/_resolvers/queries/getUserData/getUserTasks',
  'getUserTasks',
  'query'
)

Just to clarify, are the two functions getUserTasks and getUserData exported (mixed default and named) from the same file and the endpoints will be different?

OR getUserTasks will be in a file in a sub-folder (normal file-structure API?)

Just to clarify, are the two functions getUserTasks and getUserData exported (mixed default and named) from the same file and the endpoints will be different?

OR getUserTasks will be in a file in a sub-folder (normal file-structure API?)

In your app code, they would be in the same file with default and named export. But the we'd compile it to the normal nested sub-folder structure.

Okay yeah that makes sense. I would like to take a plunge at this! I have been working on the React Native support with a Babel plugin and I think that can be used for this as well.

But the we'd compile it to the normal nested sub-folder structure.

This could be a problem in one scenario: If a file exports as export * from './lib', we won't be able to statically figure out what all the named exports are, so the below method might be required to ensure we can serve that case. Static type-checking will work either way.

const getUserTasks = getResolverRpcClient(
    'app/_resolvers/queries/getUserData',
    // server handler would know how to resolve this
    'getUserData.getUserTasks',
    'query'
)

But if we don't want to support that export * from './lib' way of exporting, with other named exports, we can probably figure all of those out in a Babel plugin and create resolvers files that reexport those functions from the main resolver file.

  • Another note is that when the functions are moved to the resolvers folder, I don't see where the imports are also migrated. This could cause problems since people might not use absolute imports everywhere. But I think the Babel plugin can solve this problem as well, and fix the imports during the build step.

^ This way of exporting could be used to package up backend functionality as NodeJS functions simply and you immediately get that available as a REST API on the frontend.

@nksaraf ok!

I'm not totally following on the export * issue you are talking about.

Another note is that when the functions are moved to the resolvers folder, I don't see where the imports are also migrated. This could cause problems since people might not use absolute imports everywhere. But I think the Babel plugin can solve this problem as well, and fix the imports during the build step.

The blitz compiler moves the actual resolver code into _resolvers and replaces the original file with the isomorphic handler. And the compiler also supports relative imports.

I'm not totally following on the export * issue you are talking about.

For example, if a query file is as such (multiple named exports),

export * from '../bunch-of-functions'
export * from 'some-lib'

Then during import,

import { func1, func2 } from './queries/funcs'

Then we wont be able to figure out which resolver files / API endpoint files need to be created for individual functions without following the dependencies. That can be difficult and error prone. But the Typescript support will continue to work with this example.

That's why I was suggesting that we could let the API endpoint figure out which function to run based on the the params of the RPC call.

eg. to run func1, we would send call /funcs endpoint with func1 as the function to run and one the server side, we can import all exports from ./queries/funcs and run the required function.

import * as resolvers from 'app/_resolvers/queries/funcs';

// To access the relevant function
resolvers['func1']

The blitz compiler moves the actual resolver code into _resolvers and replaces the original file with the isomorphic handler. And the compiler also supports relative imports.

I was referring to some of the concerns regarding #1013. As files are moved around all the imports should also be fixed to make sure things don't break in build that work with Typescript. I thought Babel could be used to fix the imports

@nksaraf ah, ok yeah got it. I really think we should limit 1 function per url. However I think this should work vs generating api route files for each one:

// pages/api/funcs/[[...name]].ts
import * as resolvers from 'app/_resolvers/queries/funcs';

export default handler(req, res) {
  const name = req.query.name[0] || default
  invoke resolvers[name]
}

And then all the following endpoints work:

POST /api/funcs
POST /api/funcs/func1
POST /api/funcs/func2

Ohh yeah this is neat!

Hey @nksaraf any update on the babel stuff you was working on?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

yhoiseth picture yhoiseth  路  3Comments

flybayer picture flybayer  路  4Comments

simonedelmann picture simonedelmann  路  3Comments

ganeshmani picture ganeshmani  路  4Comments

netheril96 picture netheril96  路  4Comments