Adoption Guides
Nexus Framework Users
Hello Nexus Framework User!
If you are a Nexus framework user (nexus
package from version 0.20
to 0.27
) we strongly encourage you to migrate to the the Nexus library (nexus
package 1.0
and up). For more detail about why and how this came about please see this announcement. The following guide is written to help you transition from the framework back to the library.
Note that there is a separate migration guide detailing how to go from the Prisma plugin for Nexus Framework to the Prisma plugin for Nexus.
Bundled Dependencies
Start by uninstalling your current nexus
. Then you will need to install nexus@^1.0.0
which is now the library. You also now need to install graphql
yourself.
$npm uninstall nexus && npm install nexus graphql
JavaScript Support
If you had been using TypeScript before only because Nexus Framework forced you to, then know that you can use JavaScript with nexus
if you want. The rest of this guide will assume that you want to continue using TypeScript. Ignore or adapt instructions as needed if you are a JS user.
Managed TypeScript
You will need to manage the TypeScript toolchain yourself.
Install
typescript
,ts-node
, andts-node-dev
.$npm install -D typescript ts-node ts-node-devThe following applies to VSCode but something like it might apply to other editors as well. You must also make sure your editor is set to use the local version of TypeScript rather than the one the editor ships with. Summon the command pallet
command+shift+p
and then enter and selecttypescript: select TypeScript Version...
. Then selectWorkspace Version
.Ensure your tsconfig settings are correct.
- If you are outputting Nexus typegen to
node_modules/@types/<something>/index.d.ts
like Nexus Framework did then you must ensure that thecompilerOptions.typeRoots
andcompilerOptions.types
fields are correctly set or not set at all. IftypeRoots
is set then ensure that where you output Nexus's typegen is listed. Iftypes
is set then ensure that the parent directory where you output Nexus's typegen is listed. - If you are using Apollo Server then you must enable
skipLibCheck: true
incompilerOptions
. - Enable
strict
mode incompilerOptions
to benefit most of all from Nexus's typings.
- If you are outputting Nexus typegen to
Development Mode
You will need to manage running and restarting your server on changes during development yourself. If you are using TypeScript then use the following two development scripts dev
and dev:typecheck
. Together, these attempt to smooth over the workflow with Nexus' reflection system and approximate a subset of what nexus dev
did.
1{2 "scripts": {3 "dev": "ts-node-dev --transpile-only ./your/main/module",4 "dev:typecheck": "tsc --noEmit --watch"5 }6}
The dev
script launches a ts-node-dev
process that is responsible for running your API during development. You don't want this process to perform type checking because doing so would halt programming running, and that is incompatible with Nexus' reflection system because it depends on your program running to generate your static schema types.
The following applies to VSCode but it might apply to other editors as well: You cannot fully rely on your editor for static type checking because it only checks files that are open, not your entire project. There are some ways to configure this and additional VSCode extensions you can try to achieve this. If those don't work for you then simply run another process in a new terminal window/shell session. That is what dev:typecheck
script does. It launches a tsc
process that is responsible for type checking your code during development. You don't want this process to perform transpilation as that is already being handled by the dev
script.
Nexus Module Discovery & Singleton Schema Builder
You will need to export Nexus type definition values and import them into where you run your makeSchema
. The exact module layout you use does not matter.
In framework you could do this:
1import { schema } from 'nexus'23schema.objectType(...)
Now you will need to do something like this:
1// ./User.ts23import { objectType } from 'nexus'45export const User = objectType(...)
1// ./index.ts23import { makeSchema } from 'nexus'4import { User } from './User'56const schema = makeSchema({7 types: [User],8 ...9})
Fully Abstracted Reflection System
You need to run Nexus reflection yourself explicitly. You need to do this before running tsc
to build your app. Otherwise tsc
will fail due to type errors because of the missing typings files.
There are two primary ways you can go about this.
- Approach A: Run the subset of your app that contains the schema and that you know will exit.
- Approach B: Run your app as usual, but configure it up to exit once reflection has completed.
Approach A might be simpler if your project can accommodate it. The primary requirement to accomodate it is that this subset of the application can be run to completion. That is, you can run ts-node path/to/module
and it exits. If it doesn't exit that means you have something keeping the node process open like an awaited unresolved promise or open handle (e.g. a Prisma client connection).
Approach A is simpler in that it reduces the surface area of your codebase where you need to think about eager code running during reflection (which in turn is run before build and tests). On the other hand you may already be thinking about this if you are trying to develop testable code. Use whichever approach works best for you.
Regardless of which choice you go with, you also need to ensure that NODE_ENV=production
is set. If you forget to do this then Nexus will (by default) run reflection. This would significantly slow down your application boot time, something you don't want in serverless environments, for example.
Here is an example of approach A to managing reflection:
Layout your Nexus type defs amongst modules under a parent dir such that each module contains a part of your GraphQL schema. Put shared parts of your GraphQL schema into
shared.ts
. Re-export all type defs inindex.ts
.1/src2 /graphql3 User.ts4 Comment.ts5 shared.ts6 index.tsYour
index.ts
should import all type defs.1// src/graphql/index.ts23export * from './User'Add a
schema.ts
module that imports all type defs and callsmakeSchema
.1// src/schema.ts23export { makeSchema } from 'nexus'4import * as types from './graphql'56export const schema = makeSchema({7 types,8})Now you can run Nexus reflection by running this subset of your app e.g.
ts-node --transpile-only src/schema
. You will probably want to capture this via a new npm script called e.g.nexus:reflect
.
Here is an example of approach B to managing reflection:
Setup your app however you want
When configuring
makeSchema
make sure to enable process exit when requested via an environment variable.1import { makeSchema } from 'nexus'23export const schema = makeSchema({4 shouldExitAfterGenerateArtifacts: Boolean(process.env.NEXUS_SHOULD_EXIT_AFTER_REFLECTION),5})Now you can run Nexus reflection by running your app with that environment variable set e.g.
NEXUS_SHOULD_EXIT_AFTER_REFLECTION=true ts-node src
. You will probably want to capture this via a new script called e.g.nexus:reflect
.
Integrated Build Step With Bundling
You will need to handle the build yourself. This involves a few things:
Run Nexus reflection yourself
This has been explained in the Fully Abstracted Reflection System section.
Run
tsc
yourselfStraightforward. Note that, If you are doing Step 3 then this step may be redundant.
Optional: Run a bundler yourself
This is probably something you don't need to do. Nexus Framework took a packages-bundled approach to dependencies and this mean a large package size that bloated deployments necessitating techniques like tree-shaking (e.g. TypeScript dependency is over 50mb). This problem generally goes away when using the Nexus library directly. However bundling for serverless deployments can still be desirable especially if your non-development dependencies are heavy and not fully used at runtime. If you were relying on Nexus Framework bundling here are some tools you can try out yourself:
These tools take care of TypeScript transpilation so you should be able to skip using
tsc
manually with the above tools.When you are bundling your app you may need to tweak your
tsconfig.json
to output ES modules rather than CJS modules. The options you will need to think about aremodule
,moduleResolution
, andtarget
. There are few ways to go about this, the following is one:1{2 "compilerOptions": {3 "moduleResolution": "Node",4 "target": "ES2015",5 "module": "ES2015"6 }7}
Here is an example where bundling is not used:
1{2 "scripts": {3 "build": "yarn nexus:reflection && tsc",4 "nexus:reflection": "ts-node path/to/schema/modules/entrypoint"5 }6}
HTTP Server
Nexus framework comes bundled with Apollo Server and runs it for you. You will need to figure out your server solution yourself. One common option is Apollo Server. It is actually a middleware layer that integrates with many different HTTP frameworks and libraries. Here is an example using Apollo Server with Express.
Install new dependencies
1npm install express apollo-server-expressIf using TypeScript then Install typings for Express
1npm install -D @types/expressIf using TypeScript then you need to enable
esModuleInterop
to be able to use Apollo Server1// tsconfig.json2{3 "compilerOptions": {4 "esModuleInterop": true5 }6}Write your server code
1import { ApolloServer } from 'apollo-server-express'2import { makeSchema, objectType } from 'nexus'3import createExpress from 'express'45const schema = makeSchema({6 types: [objectType({ ... })]7})89const apollo = new ApolloServer({10 schema11})1213const express = createExpress()1415apollo.applyMiddleware({ app: express })1617express.listen(4000, () => {18 console.log(`🚀 GraphQL service ready at http://localhost:4000/graphql`)19})
HTTP Server CORS Support
Nexus Framework shipped with builtin server CORS support. You can easily achieve this yourself with Apollo Server. Following from example in the HTTP Server section, you can setup your CORS settings in the applyMiddleware
function:
1apollo.applyMiddleware({ app: express, cors: { ... } })
Integrated GraphQL Playground
You need to integrate GraphQL Playground yourself if you want it. An easy way to do this is use Apollo Server which ships with GraphQL Playground enabled in development by default.
Subscriptions Server
Nexus Frameworks automatically enables a subscriptions server when you use the GraphQL Subscription
type. One way to setup a subscriptions server yourself is with Apollo Server. For details refer to their docs.
Following from example in the HTTP Server section, here is an example with a subscriptions server:
1import * as Http from 'http'2import { ApolloServer } from 'apollo-server-express'3import { makeSchema, subscriptionType } from 'nexus'4import express from 'express'56/**7 * An example streaming data source.8 */9async function* truthStream() {10 while (true) {11 const answer = [true, false][Math.round(Math.random())]12 yield answer13 await new Promise(res => setTimeout(res, 1000))14 }15}1617/**18 * An example subscription type.19 */20const Subscription = subscriptionType({21 definition(t) {22 t.field('foobars', {23 type: 'Boolean',24 subscribe() {25 return truthStream()26 },27 resolve(eventData: boolean) {28 return eventData29 },30 })31 },32})3334const schema = makeSchema({35 types: [Subscription],36})3738const apollo = new ApolloServer({39 schema,40})4142const app = express()43const httpServer = Http.createServer(app)4445apollo.installSubscriptionHandlers(httpServer)4647apollo.applyMiddleware({ app })4849httpServer.listen({ port: 4000 }, () => {50 console.log(`server at http://localhost:4000${apollo.graphqlPath}`)51 console.log(`Subscriptions server at ws://localhost:4000${apollo.subscriptionsPath}`)52})
Global Error Handling
You will need to handle global error handling yourself. Nexus framework previously injected code that would catch synchronous and asynchronous unhandled errors, log them, and exit the process. For synchronous errors, this merely emulates Node's default behavior. However for asynchronous errors, it provided behavior that Node states it will eventually have one day.
The simplest thing you can do here other than doing nothing is use make-promises-safe
.
Builtin Logger & Logging
You will need to handle logging yourself. If you liked the @nexus/logger
from Nexus Framework you can find it now as floggy
on npm. Another great logger you might consider is pino
. The following assumes floggy
but adapt as needed.
Nexus Framework has some builtin logging functionality. You can approximate it as follows.
Install your logger
1npm install floggyCreate your application logger. The logger you export here should be used across your codebase.
1import * as Floggy from 'floggy'23Floggy.settings({ filter: 'app:*, *@warn+' })4export const log = Floggy.log.child('app')Create your request logger. The idea here is to have a logger scoped to each request. You can achieve this with different loggers than
floggy
(e.g.pino
) and with different servers than Apollo Server.1import { ApolloServer } from 'apollo-server-express'2import { log } from './path/to/application/logger/module'34const server = new ApolloServer({5 context: (request) => {6 const requestLogger = log.child('request')7 requestLogger.addToContext({ ... }) // e.g. user ID8 return { log: requestLogger }9})You still need to make this type safe. See "Context API Type Discovery" section for more detail. What follows is just a code example without explanation.
1import * as Nexus from 'nexus'2import * as Floggy from 'floggy'34export type Context = {5 log: Floggy.Logger6}78Nexus.makeSchema({9 contextType: { module: __filename, alias: 'ContextModule', export: 'Context' },10 ...11})
Bundled Scalars
You need to explicitly setup all custom scalars yourself. To match what Nexus Framework does by default you need to do the following:
Install the
graphql-scalars
package1npm install graphql-scalarsSetup the
Json
andDateTime
scalars1import { makeSchema, asNexusMethod } from 'nexus'2import { DateTimeResolver, JSONObjectResolver } from 'graphql-scalars'34const jsonScalar = asNexusMethod(JSONObjectResolver, 'json')5const dateTimeScalar = asNexusMethod(DateTimeResolver, 'date')67const schema = makeSchema({ types: [jsonScalar, dateTimeScalar] })
Context API Type Discovery
In Nexus Framework the context parameter of resolver functions is automatically typed.
1import { schema } from 'nexus'23schema.addToContext(() => { qux: 1 })45schema.objectType({6 name: "Foo",7 definition(t) {8 t.string("bar", {9 resolve(parent, args, ctx) {10 ctx.qux // statically typed as: number11 ...12 }13 })14 }15})
To achieve the same level of type safety you now need to do the following.
- Define and export a static context type
- Configure Nexus to use it
- Attach your data to the context. How to do this depends on the GraphQL server abstraction you are using usually. The example below uses Apollo Server.
Example:
1// 12// contextModule.ts34export type Context = {5 qux: number6}
1// 223import * as Nexus from 'nexus'4import * as Path from 'path'56Nexus.makeSchema({7 contextType: {8 module: Path.join(__dirname, './path/to/contextModule.ts'),9 alias: 'ContextModule',10 export: 'Context'11 },12 ...13})
1// 323import { ApolloServer } from 'apollo-server-express'45const server = new ApolloServer({6 context: () => ({ qux: 1 }),7})
Backing Types Type Discovery
In Nexus Framework backing types worked like this:
- Export a type from any module in your app
- Its name appears as an option among the string literal union populating the
rootTyping
field of Nexus object type definitions.
Nexus does not have this functionality. There are a few things you can do instead.
- Approach A: If you have a set of exported Backing types whose names match your GraphQL objects then you can set their
modules
insourceTypes
and Nexus will take care of the rest. - Approach B: If you have a set of Backing types whose names having a differing pattern than your GraphQL objects then supply a Regular Expression to the
typeMatch
property insourceTypes
. You can for example mapBackingUser -> User
,BackingPost -> Post
and so on. - Approach C: If you have a one-off mismatch then supply an arbitrary mapping to the
mapping
property ofsourceTypes
. - Approach D: If you want to colocate a type mapping like how it was in Nexus Framework then use the
sourceType
field on Nexus type defs. Unlike framework it accepts an object with apath
to a module andname
of the type to look for being exported from that module.
Here is an example using approach A:
1// 12// someModule.ts34export type Foo = {5 bar: string6}
1// 223import * as Nexus from 'nexus'4import * as Path from 'path'56Nexus.makeSchema({7 sourceTypes: {8 modules: [{9 module: Path.join(__dirname, 'path/to/someModule'),10 alias: 'SomeAlias'11 }],12 },13 types: [14 Nexus.queryType({15 definition(t) {16 t.list.field('foos', {17 type: 'Foo',18 resolve(parent, args, ctx) {19 return ctx.getFoos() // type-safe to backing types20 }21 })22 }23 }),24 Nexus.objectType({25 name: 'Foo', // "Foo" backing type matches to this GraphQL object26 definition(t) {27 t.string('barcola', {28 resolve(parent) => {29 return parent.bar // type safe thanks to backing types30 }31 })32 }33 })34 ],35})
Integrated System Testing
In Nexus Framework there was a testing module for system testing your app. You now need to figure this out yourself. There are several frameworks you can choose from such as jest
and ava
. This guide assumes jest
.
Install your test framework
1npm install jestIf you are a TypeScript user then install and configure
ts-jest
1npm install ts-jestWe will configure using a
jest.config.js
module in your project root. Do not use a JSON based configuration unless you know that you won't need the dynamic code used in some later steps here.1// jest.config.js23module.exports = {4 preset: 'ts-jest',5 testEnvironment: 'node',6}Setup your project to be able to soundly run one-off tests In order to run your test suite you typically need your app to be type checking. Given the Nexus reflection system, this means that we need to run the app before running the tests. To setup your project for running reflection see the section Fully Abstracted Reflection System.
Here is an example of how this might look in your project:
1{2 "scripts": {3 "test": "yarn nexus:reflection && jest",4 "nexus:reflection": "ts-node path/to/schema/modules/entrypoint"5 }6}Understand your TDD workflow option e.g.
jest --watch
In this case you need Nexus reflection to be running after every change to the source code. But this also typically triggers a new test run. That test run could be faster than reflection, leading to temporary type errors that don't matter to you. You don't want these type errors to block your TDD flow otherwise you get a 🐔🥚problem. To solve this you configure ts-jest to warn instead failing on type errors. The workflow then goes like this: 1. You start your test framework in watch mode 2. You edit your source code 3. Your source code under test runs. This in turn is running Nexus reflection. 4. Type errors from your initial test results should be ignored. Run the test suite again (e.g. injest
watch mode hitenter
). Now it is running with the complete set of types and you can trust the test results.There are some caveats here:
- Your tests need be importing the whole of your nexus modules and
makeSchema
code. Otherwise reflection will result in partially generated types that may other kinds of type errors you don't care about (for example a GraphQL string-literal-based object type reference becomes invalid). - And yes, every test run following change(s) potentially requiring two runs is not ideal
Here is the revised jest configuration you'll need.
1// jest.config.js23module.exports = {4 preset: 'ts-jest',5 testEnvironment: 'node',6 globals: {7 'ts-jest': {8 diagnostics: {9 warnOnly: true,10 },11 },12 },13}Here is how your script for starting your tdd flow in your project might look:
1{2 "scripts": {3 "tdd": "jest --watch"4 }5}- Your tests need be importing the whole of your nexus modules and
Setup your project to be able to soundly run on CI
We've added the
warnOnly
setting as a way to support TDD but now we can't be as sure things are good after one-off test that we expect to include type checking since invalid types will not halt the test from passing. This is mostly a problem in CI where we're aren't interacting with the test run.Here is one example of how you might solve this issue:
1// jest.config.js23module.exports = {4 preset: 'ts-jest',5 testEnvironment: 'node',6 globals: {7 'ts-jest': {8 diagnostics: {9 warnOnly: !Boolean(process.env.TYPECHECK),10 },11 },12 },13}1{2 "scripts": {3 "test": "yarn nexus:reflection && TYPECHECK=true jest"4 }5}You will need a way to run your app in tests. Here is one way adapted from the tutorial.
1npm install -D graphql-request1import { GraphQLClient, gql } from 'graphql-request'2import app from '...' // Adapt to your project34type TestContext = {5 client: GraphQLClient6}78export function createTestContext(): TestContext {9 let ctx = {} as TestContext1011 beforeEach(async () => {12 const port = await getPort({ port: makeRange(4000, 6000) })13 await app.start({ port }) // Adapt to your project14 Object.assign(ctx, {15 client: new GraphQLClient(`http://localhost:${port}`),16 })17 })1819 afterEach(async () => {20 await app.stop({ port })21 })2223 return ctx24}2526const ctx = createTestContext()2728it('works', async () => {29 expect(30 await ctx.client.query(gql`31 ...32 `)33 ).toMatchSnapshot()34})
Project Scaffolding
Nexus Framework had a CLI for scaffolding new projects. You can approximate this by cloning the examples repo and copying the source code of one of the projects.
Settings API
Nexus Framework has a gradual settings API with features such as automatic mapping of environment variables to settings. You can use the setset
package manually to gain back this functionality.
1npm install setset
1import * as Setset from 'setset'23type Input = {4 server?: {5 port?: number6 }7}89const settings = Ssetset.create<Input>({10 fields: {11 port: {12 initial: () => 4000,13 },14 },15})1617settings.data.server.port // 4000
Other Differences
Some differences between Nexus Framework and Nexus and cannot be easily approximated. The follow gathers these differences for your convenience.
- CLI
- Framework Plugins
- Zero config experience
- Pretty error messages during development
- TypeScript Language Service Plugin for refined autocomplete experience
- Centralized settings