Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serverless mode #782

Open
jasonkuhrt opened this issue May 1, 2020 · 34 comments
Open

Serverless mode #782

jasonkuhrt opened this issue May 1, 2020 · 34 comments
Labels
scope/server Related to the server component type/feat Add a new capability or enhance an existing one

Comments

@jasonkuhrt
Copy link
Member

Perceived Problem

  • serverless is a popular kind of deployment; nextjs, vercel, lambda, serverless frmework, google cloud functions, kubernetes functions, edge compute, apollo-micro, ...
  • nextjs integration Be usable with nextjs #648 needs serverless

Ideas / Proposed Solution(s)

  • Nexus needs to expose a "request event" handler. This is what the host server platform will call.

  • Many Node http server APIs have request handlers with request and response parameters. However it seems a lot more intuitive to have request as the input and response as asynchronous output.

    interface NexusServer {
      handle: (req: NexusServerRequest) => Promise<NexusServerResponse>
    }
  • ...But what does a user do when they want to code against their deployment platform of choice. For example from a comment in the nextjs integration issue:

     // pages/api/graphql.ts
     import Cors from 'cors'
     import { server, use } from 'nexus'
     import { nextjs } from 'nexus-plugin-nextjs'
     
     use(nextjs())
     
     export default server.handler
     
     // Initializing the cors middleware
     const cors = Cors({
       methods: ['GET', 'HEAD'],
     })
     
     // Helper method to wait for a middleware to execute before continuing
     // And to throw an error when an error happens in a middleware
     function runMiddleware(req, res, fn) {
       return new Promise((resolve, reject) => {
         fn(req, res, result => {
           if (result instanceof Error) {
             return reject(result)
           }
     
           return resolve(result)
         })
       })
     }
     
     async function handler(req, res) {
       // Run the middleware
       await runMiddleware(req, res, cors)
     
       // Rest of the API logic
       server.handle(req, res)  // <---- nexus handler here
     }
     
     export default handler
    • https://nextjs.org/docs/api-routes/api-middlewares
    • We could introduce server.platformHandle or server.handle.fromPlatform but it is unclear why we need the indirection, why we need both. It seems right now that we should just make server.handle be that.
  • The request handler should be exported for use by the host platform. Example with nextjs (with vercel):

    import { server } from 'nexus'
    const handleRequest = server.handle
    export default handleRequest

    The inability to export an import directly seems bad. But one could say either way is boilerplate and wht's really bad is doing any of that in the first place.

  • several things should change in nexus between serverful and serverless modes

    1. server.start server.stop APIs should only appear in serverful mode
    2. server jsDoc should reflect the current server mode
    3. Nexus dev mode; In serverful mode it should run the app server (what it does today). In serverless mode it should emulate a serverless platform and call the app's request handler
    4. server.express API should not appear in serverless mode. Express should not even be in the final built bundle.
    5. server settings;
      • port and host should only appear in serverful mode
      • path is for configuring the graphql endpoint path. The concept of path control applies to both serverful and serverless systems but its configuration/mental model is very different. In serverless the path may be configuration within terraform, yaml, cloud formation, file name/location (nextjs), etc. To the extend that Nexus can automate this the path configuration makes sense in serverless mode, however it stops making sense once a user needs to manually configure path in another system.
      • In serverless mode, the port used by dev mode serverless platform emulation should probably be configurable by the user. How should that work? Re-purpose port? settings.server.platform.port? settings.dev.server.platform.port? Conceptually, configuring the platform that runs the app in the app makes no sense, so... package.json#nexus.serverless.port? Or the conceptual nonsense is acceptable for the simplicity it gives for dev and transition between serverless/serverful?
      • In serverless mode, playground.path has the same issues as described for path. Additionally, if users expect to deploy playground in serverless mode, it raises the question of multiple endpoints, which in serverless, each constitutes its own app.
  • Some things should not change between serverful and serverless modes

    1. Server middleware server middleware #523 should remain abstracted from the host platform so that plugins can provide server middleware without concern for the server in use.

      This is non-trivial because certain http features are simply not possible on certain serverless platforms. How can a plugin author design their plugin for maximum portability, needing some server features, but not knowing if the plugin will be used in an environment that has that capability?

      The design problems here are not unlike prisma which abstracts different databases.

      Maybe some kind of capability modelling/matching system will need to be designed.

      This is a whole topic unto itself.

  • Some of the points above (settings.server.path, server middleware, ...) make pretty clear that a serverless mode is not enough, but a sererless platform mode is needed, wherein the particulars of a server platform are taken into account, adapted for.

References

@jasonkuhrt jasonkuhrt added type/feat Add a new capability or enhance an existing one scope/server Related to the server component labels May 1, 2020
@jasonkuhrt jasonkuhrt mentioned this issue May 2, 2020
2 tasks
jasonkuhrt pushed a commit that referenced this issue May 2, 2020
This feature removes documented server methods and adds three new but undocumented app methods: start, stop, assemble. We will see if we document these in a future change. Also these methods are not statically exported as named exports.

Motivations:

What has been done is mostly about refactoring.

- decouple concept of starting server from assembly to support future serverless feature #782 
- assembly is the process of gathering the lazy state into a final runnable one 
- settings is now a component like schema and server
- the components are designed to look a lot more symmetrical now
- root app state is passed down to components which attach their namespaced state to, easier to reason about, think redux
- introduce concept of checks that runs after schema assembly

BREAKING CHANGE:

- `server.start` and `server.stop` are no longer exposed. If you had a use-case for them please open an issue to discuss.
jasonkuhrt pushed a commit that referenced this issue May 6, 2020
progresses #782
closes #528 

#### Implementation Notes

Several deps have been added in this PR. The goal is to move quickly and iterate on our server components. None of the deps are large, even [fp-ts](https://bundlephobia.com/[email protected]).

fp-ts is exciting. It begins our journey into throw-free code!

#### User Notes

This feature will remain undocumented while we make progress on more parts around it.

This feature exposes request handlers on the server component.

```ts
import { server } from 'nexus'

server.handlers.graphql
server.handlers.playground
```

Users can use these to response to requests in a serverless environment.

```ts
import { server } from 'nexus'

export default (req, res) => {
  server.handlers.graphql(req, res)
}
```
@jasonkuhrt jasonkuhrt mentioned this issue May 6, 2020
14 tasks
@jasonkuhrt

This comment has been minimized.

@kristoferma

This comment has been minimized.

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented May 7, 2020

That gave me the following error

Hey @kristoferma, thanks for the kind words. Ah, I think I know why yes. We'll fix that soon, maybe tomorrow. If you file a bug issue for it that would be great too.

@Weakky We'll need to not assume that all plugin settings are JSON serializable. I kind of saw this coming but now here we are.

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented May 7, 2020

Some additional thoughts came up with @Weakky today while sprint planning.

  • Going between serverless and serverful should not be mutually exclusive. This is a significant change in our thinking.

  • How do we talk about this? Is there one app with zero or more lambdas and zero or one server? Or is there one project with zero or more serverless apps and zero or one serverful app? Does a project have an app or does app/project mean the same thing? Is it that a project has multiple apps or that an app has multiple entrypoints? "Entrypoints" instead of "apps"? ...

    • ...Thinking about this more considering that Nexus calls its default import app it seems that we should consider every entrypoint its own app and the larger context be a project.
  • In dev mode, a serverless platform emulator would run the lambda entrypoints, and the serverful entrypoint. But reflection on each could lead to different typegen. Typegen is global. There would be conflicts.

    @Weakky maybe we can look into tsconfig paths to find way to have each lambda consume only its respective typegen...

  • Build would needs to run per entrypoint. And if we cannot find solution to global typegen (see above point), then serially.

  • Nexus will need to create a bundle per lambda. This does overlap with what e.g. Next.js / Vercel provide, but we think that's ok. Just like we wanted a bundle/tree-shake step tree-shake bundle step #119 but again Next.JS also provides that. The point is integrations will always have some feature overlap with pure Nexus, and Nexus should just be as much as possible flexible enough to turn off the overlapping parts.

  • If there is a convention for serverless then we probably need a convention for serverful, e.g. server.ts becomes the serverful entrypoint for the app. For example in the following there would be three entrypoints in the app.

    	```
    	server.ts         <-- serverful entrypoint
    	lambdas/
    		graphql.ts     <-- serverless entrypoint 1
    		playground.ts  <-- serverless entrypoint 2
    	```
    
  • Re bundles, given the above example, the build result would be e.g.:

    	```
    	node_modules/
    		.build/
    			server.js          <-- (self-contained bundle)
    			lambdas/
    				graphql.js     <-- (self-contained bundle)
    				playground.js  <-- (self-contained bundle)
    	```
    
  • Plugins could tailor further to platform needs, e.g. nexus-plugin-aws-lambda could zip the lambdas etc.

  • Lambdas would use file paths to infer URL paths. Config overrides etc. would probably be available somehow. See points way below.

  • All the different entrypoints, if they constitute their own apps, will need their own app state. Changing the schema or changing the settings should apply to a specific entrypoint, not all. Of course the classic way to do this is build up state at the module level from stateless imports, then export/import everywhere to build up a graph of shared module deps where needed, etc. But that's really against the Nexus way (today). How does this problem look to Nexus. To achieve this dev mode will need to run each entrypoint in its own sub process. A very large app of dozens of entrypoints could lead to serious performance problems. But we're not sure about that. And maybe most apps will only have a few entrypoints. Anyways being limited here could be an acceptable way to start out.

    If we don't run each entrypoint in its own process we'll get the kind of problems we're seeing in nextjs dev.

  • Multiple serverless entrypoints in an app look an aweful lot like multiple serverful apps in a monorepo. ...? It feels like we can generalize the system further to accept zero-or-more serverless AND serverful entrypoints. And it feels like doing so would simplify things.

                           serverful
    
    server.ts        <-- 1 serverful app
    
    server/          <-- 1 serverful app (dir style)
      index.ts
    
    servers/         <-- N serverful app
      a.ts
      b.ts
    
    servers/         <-- N serverful app (dir style)
      a/
        index.ts
      b/
        index.ts
    
                           serverless
    
    lambda.ts        <-- 1 serverless app
    
    lambda/          <-- 1 serverless app (dir style)
      index.ts
    
    lambdas/         <-- N serverless apps
      a.ts
      b.ts
    
    lambdas/         <-- N serverless apps (dir style)
      a/
        index.ts
      b/
        index.ts
    
  • The simplicity comes from the fact that there are fewer rules to learn. There is a dimension of server kind and there is a dimension of number. No exceptions.

  • Naming is hard

    • serverless things: lambda / serverless / func / function

    • serverful things: server / process

    • ambiguous things: service

    • other things: app / project

    • I somehow like func/funcs more than lambda/lambdas...

      • easier to spell
      • easier to write
      • easier to say (feels like one less syllabol)
      func.ts        <-- 1 serverless app
      
      func/          <-- 1 serverless app (dir style)
        index.ts
      
      funcs/         <-- N serverful apps
        a.ts
        b.ts
      
      funcs/         <-- N serverful apps (dir style)
        a/
          index.ts
        b/
          index.ts
      
  • We'll need to revise the schema module auto-import system given multiple entrypoints, since, how can we know which entrypoint a schema module is associated with?

    To solve this we can use the disk as a scoping mechanism like as follows:

    server/
      index.ts        <-- app A
      graphql.ts      <-- only visible to app A
    funcs/
      b/
        index.ts      <-- app B
        graphql.ts    <-- only visible to app B
      c/
        index.ts      <-- app C
        graphql.ts    <-- only visible to app C
    common/
      graphql.ts      <-- visible to apps A B C
    

    I'm also realizing that this makes possible for a user to do something like this inside the common modules:

    import { server } from 'nexus'
    
    server.middleware(...) // all lambdas will get this!

    However if you want e.g. all modules - 1 exception then you're out of luck... unless we introduced a new apps selection api... e.g.:

    import apps from 'nexus/apps'
    
    apps('b', 'c').server.middleware(...) // app A will not get this!
    
    apps('a','b').schema.objectType(...) // app C will not get this!

    But this feels odd to me right now... probably we can drop this train of thought for now.

  • Thinking more about the conventional naming strategies. We haven't explored prefixes/suffixes.

    server.ts        <-- 1 serverful app
    
    server/          <-- 1 serverful app (dir style)
      index.ts
    
    a.server.ts      <-- OR N serverful apps
    b.server.ts
    
    a.server/        <-- OR N serverful apps (dir style)
      index.ts
    b.server/
      index.ts
    
    func.ts        <-- 1 serverless app
    
    func/          <-- 1 serverless app (dir style)
      index.ts
    
    a.func.ts      <-- N serverful apps
    b.func.ts
    
    a.func/        <-- N serverful apps (dir style)
      index.ts
    b.func/
      index.ts
    

    In order to support better grouping in file trees etc. we might want prefix instead:

    server.ts        <-- 1 serverful app
    
    server/          <-- 1 serverful app (dir style)
      index.ts
    
    server.a.ts      <-- OR N serverful apps
    server.b.ts
    
    server.a/        <-- OR N serverful apps (dir style)
      index.ts
    server.b/
      index.ts
    
    func.ts        <-- 1 serverless app
    
    func/          <-- 1 serverless app (dir style)
      index.ts
    
    func.a.ts      <-- N serverful apps
    func.b.ts
    
    func.a/        <-- N serverful apps (dir style)
      index.ts
    func.b/
      index.ts
    

    Removing a [forced] level of nesting in project layout is great. However, for serverless funcs, has the issue of complicating the file-systems-as-url.

    Before

    funcs/
      foo/
        qux/
          bar.ts   <-- POST /foo/qux/a
    

    After??

    func.a/
      foo/
        qux/
          index.ts   <-- POST /foo/qux/a ????
    

    Ultimately I'm not sure the file-system-as-url-path is a great idea, though. It is very rigid. For example maybe a handler wants to be available at multiple paths or methods...
    Maybe this is where Nexus draws the line and we stick to APIs, e.g.:

    // func.a.ts
    import { func } from "nexus";
    
    func.routes.add({
      method: "POST",
      path: "/a/b/c",
    });
    
    func.routes.add({
      method: "GET",
      path: "/z/y/x",
    });

    or e.g.

    // func.a.ts
    import { settings } from 'nexus'
    
    settings.change({
      func: {
        routes: {
          method: 'POST',
          path: '/a/b/c',
        }, {
          method: 'GET',
          path: '/z/y/x'
        }
      }
    })

    or e.g. maybe Nexus has a centralized routing table system:

    // imagine this module is in some common/top-level place in the project
    // e.g. routing.ts
    
    import { routing } from 'nexus/funcs'
    
    routing.a.add({ methods: ['POST', 'GET'], path: '/a/b/c' })
    routing.b.add({ method: 'POST', path: '/a/b/z' })
    routing.c.add({ method: 'DELETE', path: '/a/b/foo' })
    //      ^----- app names, typegen

    Then Nexus would use reflection to extract data and generate the config actually needed in the e.g. serverless.yml etc. file.

    In fact, even for a NextJS integration, maybe Nexus could generate modules into /api !?

    Unlike working with the file-system an API approach is type safe, works with IDE workflows, including refactoring and so on. It seems to scale much better.

    @Weakky IIUC this was more or less your position on the call today right? I think I'm onboard now.

@kristoferma
Copy link

Next.js encourages users to fetch initial props for apps via getServerSideProps method of each page component. They encourage users not to use api routes but to "write server side code directly" docs. This would require a new way of interfacing with nexus without launching a server.

The easiest way to do this is to expose a method to execute graphql queries without starting a server. Something like this:

import { execute } from 'nexus'

export const getServerSideProps = async ({ params }) => {
  const response = execute(params.query, params.variables)
  return JSON.stringify(response)
}

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented May 16, 2020

Hey @kristoferma, yeah we're aware, from the docs:

That means you can write code such as direct database queries without them being sent to browsers. You should not fetch an API route from getStaticProps — instead, you can write the server-side code directly in getStaticProps.

This means that one should be using, for example, and generally, Prisma Client directly, not an API at all.

Of course, that's the general idea. A user in should do what they need to in their scenario.

interfacing with nexus without launching a server.

Using Nexus without a server is already possible https://www.nexusjs.org/#/guides/serverless

@jasonkuhrt
Copy link
Member Author

Hey @kristoferma sorry missed your post before, about it:

use(prisma()) gave me the Error: You most likely forgot to initialize the Prisma Client. error. I bypassed that by doing

This was a bug, its fixed now in latest canary.

That gave me the following error

Plugin settings reflection was enhanced to support non-serializable settings data. So again, latest canary this is fixed.

@kristoferma
Copy link

Plugin settings reflection was enhanced to support non-serializable settings data. So again, latest canary this is fixed.

Thanks, this fixed the issue for me

This means that one should be using, for example, and generally, Prisma Client directly, not an API at all.

I should have explained my use case better. I am using Relay with Next.js and I want to server side render my page and pass on the Relay Store caching from the server to the client. To do this I must fetch the data with relay on the server, stringify it as json and pass it on as props via GetServerSideProps. If I use Prisma Client directly, I lose the ability to pass the Relay store from the server to the client.

Currently I fetch the data from the graphql endpoint and it works fine, but it adds an extra step that could be bypassed by executing the graphql query from the GetServerSideProps

@jasonkuhrt
Copy link
Member Author

@kristoferma Ah my mistake, you were talking about getServerSideProps and I replied about getStaticProps.

Currently I fetch the data from the graphql endpoint and it works fine, but it adds an extra step that could be bypassed by executing the graphql query from the GetServerSideProps

It seems like what you need is the layer right below the handler. You might be able to hack a solution right now with Nexus' current serverless support.

Your use-case is interesting. If you can create a new issue for that'd be great.

@toddpla
Copy link
Contributor

toddpla commented May 24, 2020

Hello @jasonkuhrt, I am enjoying getting to grips with nexus and would like to deploy with lambda. If this is already possible then it would be useful to reference an example lambda handler function. Perhaps I am jumping ahead slightly though. Thanks

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented May 25, 2020

Perhaps I am jumping ahead slightly though

Hey, yeah slightly. You're kind of on your own right now. I actually think it might be possible now with the primitives we've shipped but, not sure. Check out https://www.nexusjs.org/#/guides/serverless.

Now that we have an integrated bundle step, it means you should be able to zip the build and ship to AWS Lambda. But again we haven't explicitly made this a feature/goal yet, its on the roadmap.

@zapbr
Copy link

zapbr commented Jun 3, 2020

I'm trying to use with Serverless AWS but I've problem with the export handlers because the graphQL handler cant load all the schema files.

▲ nexus:schema Your GraphQL schema is empty. This is normal if you have not defined any GraphQL types yet. If you did however, check that your files are contained in the same directory specified in the `rootDir` property of your tsconfig.json file.

If we access the playground using the yarn start it works.

In serverless it works too, but It cant find any of the schema files. The playground works but empty.

Manually changing the deployed files and moving the require files from the index.js to app.js solves the issue.

There is a copy of the app.ts

// app.ts
import app, { use, server } from 'nexus'
import { prisma } from 'nexus-plugin-prisma'
import serverless from 'serverless-http'

use(prisma())

app.assemble()

export const graphql = serverless(server.handlers.graphql, {
    request(request: any, event: any, context: any) {
        const { body } = request as any

        request.context = event.requestContext;
        request.body = JSON.parse(body.toString()) // parsing body bc body is some weird buffer thing

        return request;
    }
})

export const playground = serverless(server.handlers.playground)

Is it somehow possible to bundle the schema files into the built app.js?

@kristoferma
Copy link

This happens for me because there are two instances of Prisma, one in nexus-plugin-prisma node_modules and another one in the root project node_modules.

I fix this by deleting node_modules/nexus-plugin-prisma/node_modules/.prisma after npm install

This is probably a bug but I have not submitted an issue on this

@zapbr
Copy link

zapbr commented Jun 3, 2020

This happens for me because there are two instances of Prisma, one in nexus-plugin-prisma node_modules and another one in the root project node_modules.

I fix this by deleting node_modules/nexus-plugin-prisma/node_modules/.prisma after npm install

This is probably a bug but I have not submitted an issue on this

This not solve my issue, sadly.

In my environment I don't have this duplicated Prisma.

Pretty sure is because we are missing the schema required files on the handler call.

In the non-index.js entrypoint we have to use in order to export the handlers.

@jasonkuhrt
Copy link
Member Author

@kristoferma

and another one in the root project node_modules.

Nexus Prisma apps depending on prisma deps directly is not supported.

@zapbr

Is it somehow possible to bundle the schema files into the built app.js?

That's what nexus build effectively does, but it preserves the file tree.

@kldzj
Copy link

kldzj commented Jun 4, 2020

@jasonkuhrt the necessary schema files are bundled into index.js, which does not contain any of the exported handlers from app.ts. So in order to use nexus with the serverless framework, currently my understanding is that we need to copy all the require statements into the built app.js (manual retouching of built files).

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented Jun 4, 2020

@kldzj will have to look into it. Like I mentioned to toddpla you're a bit ahead of us. If you want to contribute a serverless example to examples repo that might be a good place to explore the issue more precisely.

@zapbr
Copy link

zapbr commented Jun 4, 2020

Hallo @jasonkuhrt, thank you for your quick response.
So we build a little demo with the issue:

https://github.com/zapbr/nexus-prisma-aws-serverless-demo

@jasonkuhrt jasonkuhrt pinned this issue Jun 25, 2020
@jasonkuhrt jasonkuhrt changed the title serverless mode Serverless mode Jul 8, 2020
@AaronBuxbaum
Copy link

This is a hack, but if you just import your schema files, you'll force it into the bundle.

import app, { server, use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import * as serverless from "serverless-http";
import "./graphql/User"; // hack to resolve https://github.com/graphql-nexus/nexus/issues/782

use(prisma({ features: { crud: true } }));

app.assemble();

Hoping for a better solution soon!

@kldzj
Copy link

kldzj commented Jul 25, 2020

@AaronBuxbaum great find, but seems very ugly when you have a lot of models. We decided to just migrate back to @nexus/schema.

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented Jul 27, 2020

@AaronBuxbaum that's with nextjs correct?

@kldzj I'm assuming there was another reason than that since you'll have to import modules just the same with @nexus/schema. What was the reason in your case?

@AaronBuxbaum
Copy link

@AaronBuxbaum that's with nextjs correct?

Currently, no (I'm playing with serverless -> AWS Lambda), but I suspect it would be easy to put this on NextJS if desired

@kldzj
Copy link

kldzj commented Jul 27, 2020

@jasonkuhrt wanting to use federation was another reason, but mainly for ease of serverless deployment.

@jasonkuhrt
Copy link
Member Author

@AaronBuxbaum If you can contribute an example or recipe that would be great. It seems that your technique is similar to our nextjs recipe.

@sfratini
Copy link

I made it work with Webpack + Serverless + Lambda but I am not sure if the solution is feasible. Basically, like the #109 issue says, you get a lot of warnings if you use sls and webpack, because of the usage of require.resolve(). However, if you wrap it on eval() those are executed at runtime and they seem to work just fine (it took me like 50 webpack google searches to get there). So, I manually modified the files with warnings and I got it to work locally with sls offline and I also was able to deploy to Lambda, however you get the error from the require right now, because the code does not include the fix. I'll try to bundle manually and see if I can confirm this, however, here are some snippets in case it helps:

Note: I could not make the typescript serverless plugin to work so I run tsc, prisma generate and nexus build before deploy.

//app.ts
import app, { use, settings, server} from 'nexus'
import { prisma } from 'nexus-plugin-prisma'
import serverless = require('serverless-http')
import './graphql/graphql'; //Here is my Nexus schema definitions

settings.change({
    logger: {
        pretty: true
    },

    server: {
        startMessage: (info) => {
        settings.original.server.startMessage(info)
        },
    },

    schema: {
        generateGraphQLSDLFile: './graphql/schema.graphql'
    }

})

use(
    prisma(
        {
            migrations: false,
            features: {
                crud: true
            }
        }
    )
)

app.assemble()

export const graphql = serverless(server.handlers.graphql, {
    request(request: any, event: any, context: any) {
        const { body } = request as any

        request.context = event.requestContext;
        request.body = JSON.parse(body.toString()) // parsing body bc body is some weird buffer thing

        return request;
    }
})

export const playground = serverless(server.handlers.playground)
//serverless.yml
service: graphql-lambda

plugins:
#  - serverless-plugin-typescript  
  - serverless-webpack
  - serverless-offline

custom:
  prune:
    automatic: true
    number: 5
  serverless-offline:
    port: 1337
  webpack:
    webpackConfig: 'webpack.config.js'   # Name of webpack configuration file
    includeModules: true # Node modules configuration for packaging #I dont think this is needed anymore
    packager: 'yarn'   # Packager that will be used to package your external modules

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev # Set the default stage used. Default is dev
  region: us-east-1 # Overwrite the default region used. Default is us-east-1
  profile: production # The default profile to use with this service
  memorySize: 512 # Overwrite the default memory size. Default is 1024
  deploymentBucket:
    name: com.serverless.${self:provider.region}.deploys # Overwrite the default deployment bucket
    serverSideEncryption: AES256 # when using server-side encryption
    tags: # Tags that will be added to each of the deployment resources
      environment: ${self:provider.stage}
      service: serverless
  deploymentPrefix: serverless # Overwrite the default S3 prefix under which deployed artifacts should be stored. Default is serverless
  versionFunctions: true #false # Optional function versioning

functions:
  playground:
    handler: app.playground
    events:
        - http:
              path: playground
              method: get
              cors: true
  graphql:
    handler: app.graphql
    events:
    - http:
        path: graphql
        method: get
        cors: true
    - http:
        path: graphql
        method: post
        cors: true
//webpack.config.js
const slsw = require('serverless-webpack');
const CopyPlugin = require('copy-webpack-plugin')
const path = require('path');

module.exports = {
    entry: slsw.lib.entries,
    target: 'node',
    mode: slsw.lib.webpack.isLocal ? "development": "production",
    output: {
      libraryTarget: 'commonjs',
      path: path.join(__dirname, '.webpack'),
      filename: '[name].js',
      pathinfo: false,
    },
    optimization: {
      minimize: false
    },
    plugins: [
      new CopyPlugin({
        patterns: [
          { from: './prisma/schema.prisma' },
          { from: './.nexus', to: './.nexus'},
          { from: './node_modules/.prisma', to: './node_modules/.prisma'},
          { from: './node_modules/nexus-plugin-prisma', to: './node_modules/nexus-plugin-prisma'},
        ]
      })
    ]
  };

Example that I had to change in typegenAutoConfig.js

resolvedPath = eval(`require.resolve("${pathOrModule}", {
                    paths: [${process.cwd()}],
                })`);

I also had to make similar changes on files like manifest.js, linkable.js, couple of utils.js, import.js, etc.

After this, it will complain that it cannot find the core, so I also had to change the import order on @nexus/schema/dist-esm/index.js, so this line is at the end, after the imports:

export { core, blocks, ext };

After this, sls offline will work just fine and I can query the database normally. Also, my deployed function on Lambda is about 21mb which is completely fine.

As I said, this does not work just yet on deployment. It will complain about the plugin not being able to find because I have to assume sls is just doing yarn and getting the npm module which does not have the fix. I'll try to work on that next.

@heibel
Copy link
Contributor

heibel commented Aug 6, 2020

Dropping by to say I have a blast combining NextJS, Prisma and Nexus with the experimental serverless API. My /api/graphql endpoint looks something like this:

// pages/api/graphql.ts

import app, { settings } from "nexus";
import nextConnect from "next-connect";

import { sessionMiddleware, randomMiddleware } from "../../lib/middleware";

require("../../graphql/schema");

settings.change({
  server: {
    path: "/api/graphql",  // for playground
  },
});

app.assemble();

export default nextConnect()
  .use(sessionMiddleware)
  .use(randomMiddleware)
  .use(app.server.handlers.graphql);

Thank you for your hard and much appreciated work on Nexus

@hienlh
Copy link

hienlh commented Aug 9, 2020

I tried all of code above but I can not run my serverless. Does Nexus support serverless?

@sfratini
Copy link

@heibel Could you please share the rest of your setup? I am still trying to deploy to lambda and while it works locally, it is because of the manual changes that I did to the code and I could not make the setup to work effortlessly. Thanks

@heibel
Copy link
Contributor

heibel commented Aug 17, 2020

@sfratini I forgot to mention I deploy to/with Vercel. Which works with the above code. Maybe you can share your code/lambda errors?

@sfratini
Copy link

@sfratini I forgot to mention I deploy to/with Vercel. Which works with the above code. Maybe you can share your code/lambda errors?

Sure, so basically it is not finding the prisma plugin. Now, the plugins are loaded using require.resolve which webpack does not like. I "solved" that locally by replacing all the places nexus uses that, with eval() calls which are executed at runtime (you can see my changes a couple of comments above). The issue however, is that serverless does an install which, obviously installs the version without the change so on the cloud, the plugins won't work. (with webpack)

I need to use webpack (or any other packager/compression tool), because lambda has a 250MB limit.

  2020-08-17T09:35:48.525+02:00 �[90m 57 �[39m�[31m✕ �[39m�[31mnexus�[39m There were errors loading 1 or more of your plugins.
 

 

@ben-walker
Copy link

I've been having a blast playing around with Nexus + Serverless for the last few days, thought I'd throw my solution out there in case it helps anyone. Still a WIP, but I'm pretty happy with what I've got so far (Prisma client operational on AWS Lambda, doesn't require webpack or any request hacking).

api/app.ts

import app, { use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import serverless from "serverless-http";
import "./graphql"; // Force injection of schema into app bundle (this is just an index.ts in the GraphQL module)

app.assemble();

export const graphqlFunc = serverless(app.server.express); // Make sure to use express rather than the graphql handler

prisma/schema.prisma

...

generator client {
  provider      = "prisma-client-js"
  // Add "rhel-openssl-1.0.x" as a binary target (AWS needs this)
  binaryTargets = ["native", "rhel-openssl-1.0.x"]
}

serverless.yaml

...

# We only include the .nexus/ directory, which keeps the size within Lambda limits
package:
  exclude: ["**"]
  include: [.nexus/**]

functions:
  graphql:
    handler: .nexus/build/api/app.graphqlFunc
    events:
      - http:
          path: /{proxy+}
          method: any

I'm still trying to wrap my head around injecting a different DATABASE_URL in different environments, and running migrations/seeds post-deployment, but I think the broad strokes are there!

@sfratini
Copy link

I've been having a blast playing around with Nexus + Serverless for the last few days, thought I'd throw my solution out there in case it helps anyone. Still a WIP, but I'm pretty happy with what I've got so far (Prisma client operational on AWS Lambda, doesn't require webpack or any request hacking).

api/app.ts

import app, { use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import serverless from "serverless-http";
import "./graphql"; // Force injection of schema into app bundle (this is just an index.ts in the GraphQL module)

app.assemble();

export const graphqlFunc = serverless(app.server.express); // Make sure to use express rather than the graphql handler

prisma/schema.prisma

...

generator client {
  provider      = "prisma-client-js"
  // Add "rhel-openssl-1.0.x" as a binary target (AWS needs this)
  binaryTargets = ["native", "rhel-openssl-1.0.x"]
}

serverless.yaml

...

# We only include the .nexus/ directory, which keeps the size within Lambda limits
package:
  exclude: ["**"]
  include: [.nexus/**]

functions:
  graphql:
    handler: .nexus/build/api/app.graphqlFunc
    events:
      - http:
          path: /{proxy+}
          method: any

I'm still trying to wrap my head around injecting a different DATABASE_URL in different environments, and running migrations/seeds post-deployment, but I think the broad strokes are there!

This is very interesting. As for the URL, I would use Lambda environment variables or WKS. I never understood why the env variables in lambda cannot be protected a little more. Anyone with read access to the function will see those.

So every dependency that nexus needs is in there? What about prisma client? resolvers? The nexus build actually works as webpack and generates an isolated folder?

@ben-walker
Copy link

Yup, everything nexus needs should be in that .nexus/build/ directory. My deploys to AWS were failing due to size constraints, and I noticed node_modules at the root of the Lambda, and node_modules in .nexus/build/; turns out you only need one set!

The Prisma Client should also be in .nexus/build/node_modules, under .prisma. As long as you run npm run build before your serverless deploy, the resolvers/prisma client/node_modules should all be there as required.

Yeah my solution for the DATABASE_URL was as follows, seems ok but it's in plaintext in the AWS Console so still not ideal :(

  • Set DATABASE_URL as a GitHub Secret
  • Make that secret available in GitHub AWS deploy action, when running sls deploy
  • In serverless.yaml, add environment: DATABASE_URL: ${env:DATABASE_URL}, which will read from the GitHub secret during the deploy

With the above everything is working (and also allows for migrations in the GitHub deploy action). I can access the GraphQL playground and query the DB in AWS. Eventually should maybe use AWS SSM for the DB URL though. Hope that helps!

@frankyveras2
Copy link

frankyveras2 commented Sep 21, 2020

Hello, I've been playing with Nexus + Prisma + Serverless for a few days as well, I want to share my working solution

graphql.ts

`
import { settings, use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import { PrismaClient } from "@prisma/client";
import { join } from "path";

// Enable nexus prisma plugin with crud features
use(
prisma({
migrations: true,
features: { crud: true },
client: { instance: new PrismaClient() },
})
);

settings.change({
schema: {
connections: {
default: {
includeNodesField: true,
cursorFromNode: (node, args, ctx, info, { index, nodes }) => {
return node.id;
},
},
},
generateGraphQLSDLFile: join(process.cwd(), "/generated/schema.graphql"),
},
});`

app.ts

`
import app, { use, settings, server } from "nexus";
import serverless from "serverless-http";
import * as bodyParser from "body-parser-graphql";
import raw from "raw-body";
import inflate from "inflation";

require(./pathToObjectTypes) // Here import all your object types schema

settings.change({
logger: {
pretty: true,
},
server: {
playground: true,
graphql: { introspection: true },
cors: true,
},
});

app.assemble();

export const graphql = serverless(server.handlers.graphql, {
async request(request: any, event: any, context: any) {
const { body } = request;
request.context = event.requestContext;
// this is required because it's giving a weird error when trying to parse the body of the request, check by yourself without it on the playground
if (request.headers["content-type"] === "application/json") {
const str = await raw(inflate(request), { encoding: "utf8" });
request.body = JSON.parse(str.toString());
}
return request;
},
});
`

Serverless.yml

`
package:
exclude:

  • ./**
  • '!.nexus/build/**'

functions:
graphql:
handler: .nexus/build/src/app.graphql
environment:
GOOGLE_API_KEY: XXXXXXXXXXXXXXXX
events:
- http:
path: /
method: post
cors: true
integration: lambda-proxy

  - http:
      path: /
      method: get
      cors: true
      integration: lambda-proxy

`

@jasonkuhrt jasonkuhrt unpinned this issue Sep 30, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope/server Related to the server component type/feat Add a new capability or enhance an existing one
Projects
None yet
Development

No branches or pull requests