Skip to main content
Version: Draft

Using PostGraphile as a Library

Library mode is the most popular way of running PostGraphile; it gives more power than using the CLI (see CLI usage) because you can leverage the capabilities and ecosystems of your chosen Node.js webserver (Express, Koa, Fastify, etc), but is more fully featured than Schema-only Usage.

PostGraphile instance

Library mode is configured using a preset (see Configuration for the options) and returns a PostGraphile instance pgl which has various methods you can use depending on what you're trying to do.

pgl.js
import preset from "./graphile.config.js";
import { postgraphile } from "postgraphile";

// Our PostGraphile instance:
export const pgl = postgraphile(preset);

pgl.createServ(grafserv)

Grafserv supports a number of different servers in the JS ecosystem, you should import the grafserv function from the relevant grafserv subpath:

import { grafserv } from "postgraphile/grafserv/express/v4";
// OR: import { grafserv } from "postgraphile/grafserv/node";
// OR: import { grafserv } from "postgraphile/grafserv/koa/v2";
// OR: import { grafserv } from "postgraphile/grafserv/fastify/v4";

Then create your serv instance by passing this to the pgl.createServ() method:

const serv = pgl.createServ(grafserv);

This Grafserv instance (serv) can be mounted inside of your chosen server - for instructions on how to do that, please see the relevant entry for your server of choice in the Grafserv documentation; typically there's a serv.addTo(...) method you can use.

Here's an example with Node's HTTP server:

example-node.js
import { createServer } from "node:http";
import { grafserv } from "postgraphile/grafserv/node";
import { pgl } from "./pgl.js";

const serv = pgl.createServ(grafserv);

const server = createServer();
server.once("listening", () => {
server.on("error", (e) => void console.error(e));
});

serv.addTo(server).catch((e) => {
console.error(e);
process.exit(1);
});

server.listen(5678);

console.log("Server listening at http://localhost:5678");

And an example for Express:

example-express.js
import { createServer } from "node:http";
import express from "express";
import { grafserv } from "postgraphile/grafserv/express/v4";
import { pgl } from "./pgl.js";

const serv = pgl.createServ(grafserv);

const app = express();
const server = createServer(app);
server.once("listening", () => {
server.on("error", (e) => void console.error(e));
});
serv.addTo(app, server).catch((e) => {
console.error(e);
process.exit(1);
});
server.listen(5678);

console.log("Server listening at http://localhost:5678");

For information about using this serv instance with Connect, Express, Koa, Fastify, Restify, or any other HTTP servers, please see the Grafserv documentation.

pgl.getSchemaResult()

Returns a promise to the schema result — an object containing:

  • schema - the GraphQL schema
  • resolvedPreset - the resolved preset

Note that this may change over time, e.g. in watch mode.

pgl.getSchema()

Shortcut to (await pgl.getSchemaResult()).schema — a promise to the GraphQL schema the instance represents (may change due to watch mode).

pgl.getResolvedPreset()

Get the current resolved preset that PostGraphile is using. Synchronous.

pgl.release()

Call this when you don't need the PostGraphile instance any more and it will release any resources it holds (for example schema watching, etc).

Example 1: single JS file, express

In this example we'll set up a PostGraphile server using ExpressJS all in a single JS file that we can run directly with node.

To start, create a directory for the project and initialize a Node.js project in that folder:

Create and initialize the project directory
mkdir postgraphile_express
cd postgraphile_express
npm init -y

Installing dependencies

Install the required packages:

npm install --save express postgraphile@beta

Environment variables

It's bad practice to store credentials directly into source code, so instead we use an environment variable DATABASE_URL to to store the database connection string.

Additionally, we want PostGraphile to behave slightly differently in development versus production: in development we want better error messages and easier to read SQL; whereas, in production, we don't want to reveal more information to potential attackers than we need to, and we want to execute as fast as possible. Since we're in development, we'll use the GRAPHILE_ENV=development envvar to indicate this.

To save us from having to pass the environment variables every time we run the server, we can add them to a convenient .env file:

.env
# Replace the contents of the square brackets with the details of your database
DATABASE_URL=postgres://[username]:[password]@[host]:[port]/[database]
GRAPHILE_ENV=development

Be sure to replace the square brackets with the relevant settings for your own database connection!

Critically, we must ensure that this file is not tracked by git:

Ensure the .env file is ignored by git
echo .env >> .gitignore

The code

Create a server.js file with the following contents:

server.js
import express from "express";
import { createServer } from "node:http";
import { postgraphile } from "postgraphile";
import { PostGraphileAmberPreset } from "postgraphile/presets/amber";
import { makePgService } from "postgraphile/adaptors/pg";
import { grafserv } from "postgraphile/grafserv/express/v4";

// Which port do we want to listen for requests on?
const PORT = 5050;

// Our PostGraphile configuration, we're going (mostly) with the defaults:
/** @type {GraphileConfig.Preset} */
const preset = {
extends: [PostGraphileAmberPreset],
pgServices: [
makePgService({
connectionString: process.env.DATABASE_URL,
schemas: ["public"],
}),
],
grafast: {
explain: true,
},
};

// Create our PostGraphile instance, `pgl`:
const pgl = postgraphile(preset);

// Create our PostGraphile grafserv instance, `serv`:
const serv = pgl.createServ(grafserv);

async function main() {
// Create an express app:
const app = express();

// Create a Node HTTP server, and have the express app handle requests:
const server = createServer(app);

// If the server were to produce any errors after it has been successfully
// set up, log them:
server.once("listening", () => {
server.on("error", (e) => void console.error(e));
});

// Mount our grafserv instance inside of the Express app, also passing the
// reference to the Node.js server for use with websockets (for GraphQL
// subscriptions):
await serv.addTo(app, server);

// Start listening for HTTP requests:
server.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
});
}

// Start the main process, exiting if an error occurs during setup.
main().catch((e) => {
console.error(e);
process.exit(1);
});

The project is now complete, listing the project directory should show

postgraphile_express/
├── .env
├── .gitignore
├── node_modules/
├── package-lock.json
├── package.json
└── server.js

The package.json file should have the content similar to the following (with the versions of express and postgraphile being the most recent):

{
"name": "postgraphile_express",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.2",
"postgraphile": "^5.0.0-beta.38"
}
}

To tidy up, let's make the following changes to the package.json file:

 {
"name": "postgraphile_express",
"version": "1.0.0",
- "main": "index.js",
+ "private": true,
+ "type": "module",
"scripts": {
+ "start": "node --env-file=./.env server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
- "keywords": [],
- "author": "",
- "license": "ISC",
- "description": "",
"dependencies": {
"express": "^4.21.2",
"postgraphile": "^5.0.0-beta.38"
}
}

The main things we've done here are:

  • Remove the reference to a non-existant index.js file
  • Mark the project as "private" such that we can't attempt to npm publish it to the npm repository
  • Declare that .js files are ES modules, enabling the use of import statements
  • Delete unnecessary metadata
  • Add our start script, which runs the server contained in server.js using the node command, passing the environment variables stored in the .env file

Running the server

Thanks to the start script we added above, running our server is as simple as:

npm start

Alternatively you can run the command from the start script directly:

node --env-file=./.env server.js

Either way, this will start a server at http://localhost:5050. Opening the URL in a browser will show a user interface where a GraphQL query can be entered. The following query is valid against any GraphQL schema and tells you the fields that are available to be queried at the Query root:

query {
__schema {
queryType {
name
fields {
name
description
}
}
}
}

Example 2: TypeScript project, express

To get the most out of PostGraphile (and the modern npm ecosystem in general), you'll want to be running TypeScript. This will give you much better auto-complete in your editor, type safety of your code, help with refactoring, and so much more.

This example will have three files in a src directory: graphile.config.ts, pgl.ts and server.ts. It will also demonstrate including the simple-inflection preset, included as PgSimplifyInflectionPreset in graphile.config.ts

To start, create a directory for the project and initialize a Node.js project in that folder:

Create and initialize the project directory
mkdir postgraphile_express_typescript
cd postgraphile_express_typescript
npm init -y

Installing dependencies

Install the required packages:

npm install --save express postgraphile@beta @graphile/simplify-inflection@beta
npm install --save-dev typescript @tsconfig/node22 @types/express @types/node

Environment variables

As before, create a .env file containing your database connection string and development/production environment setting:

.env
DATABASE_URL=postgres://[username]:[password]@[host]:[port]/[database]
GRAPHILE_ENV=development

And ensure that it is not tracked by git:

Ensure the .env file is ignored by git
echo .env >> .gitignore

TypeScript configuration

Create a tsconfig.json file that extends from the relevant @tsconfig for your Node.js major version; e.g. if you're using Node v22.14.0 that would be @tsconfig/node22:

tsconfig.json
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"erasableSyntaxOnly": true,
"rewriteRelativeImportExtensions": true,
"rootDir": "./src",
"outDir": "./dist"
}
}

The code

In the postgraphile_express_typescript directory, create a src directory and create three TypeScript files in the src folder: graphile.config.ts, pgl.ts and server.ts with the contents shown below:

src/graphile.config.ts

src/graphile.config.ts
import type {} from "graphile-config";
import "postgraphile";
import { makePgService } from "postgraphile/adaptors/pg";
import { PostGraphileAmberPreset } from "postgraphile/presets/amber";
import { PgSimplifyInflectionPreset } from "@graphile/simplify-inflection";

const preset: GraphileConfig.Preset = {
extends: [PostGraphileAmberPreset, PgSimplifyInflectionPreset],
pgServices: [
makePgService({
connectionString: process.env.DATABASE_URL,
schemas: ["public"],
}),
],
grafast: {
explain: true,
},
};

export default preset;

src/pgl.ts

src/pgl.ts
import { postgraphile } from "postgraphile";
import preset from "./graphile.config.ts";

export const pgl = postgraphile(preset);
Import from .ts along with rewriteRelativeImportExtensions

With Node's new --experimental-strip-types flag, TypeScript syntax is removed so that the TS can be executed directly as if it were JS. However, the files still need to be able to reference each other. In the source code, that means referencing the .ts file; but when TypeScript compiles the code for production the output will be .js files.

Fortunately, TypeScript has added the configuration option rewriteRelativeImportExtensions to ensure that you can use .ts to reference TypeScript files in your source code whilst still ensuring that these imports are output as .js when compiled for production usage.

src/server.ts

src/server.ts
import { createServer } from "node:http";
import express from "express";
import { grafserv } from "postgraphile/grafserv/express/v4";
import { pgl } from "./pgl.ts";

const serv = pgl.createServ(grafserv);

const app = express();
const server = createServer(app);
server.once("listening", () => {
server.on("error", (e) => void console.error(e));
});
serv.addTo(app, server).catch((e) => {
console.error(e);
process.exit(1);
});
server.listen(5050);

console.log("Server listening at http://localhost:5050");

Contents of package.json

After making similar changes to package.json as we did with Example 1 above, we should end up with a package.json file that looks something like:

package.json
 {
"name": "simple_node_project",
"version": "1.0.0",
+ "private": true,
+ "type": "module",
"scripts": {
+ "start": "node --env-file=./.env src/server.ts",
+ "build": "tsc",
+ "prod": "node dist/server.js"
},
"dependencies": {
"@graphile/simplify-inflection": "^8.0.0-beta.6",
"express": "^4.21.2",
"postgraphile": "^5.0.0-beta.38"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.0",
"@types/express": "^5.0.0",
"@types/node": "^20.11.24",
"typescript": "^5.7.3"
}
}

Note that we have three scripts:

  • start runs our source files directly using Node 22's type stripping
  • build compiles our .ts files to .js files to run in production
  • prod runs the compiled .js files in production, and expects the environment variables to already be set in the environment rather than reading them from a file

Project Structure

The project structure should be

postgraphile_express_typescript/
├── .env
├── .gitignore
├── node_modules/
├── package-lock.json
├── package.json
├── src/
│ ├── graphile.config.ts
│ ├── pgl.ts
│ └── server.ts
└── tsconfig.json

Development

In development, run npm start as before. This will run the source code directly using Node's native type stripping feature. This will start a server at http://localhost:5050, opening the url in a browser will show a user interface where a GraphQL query can be entered, for example

query {
__schema {
queryType {
name
fields {
name
description
}
}
}
}
Errors running npm start?

If you get errors when running npm start it might be because you are not running a sufficiently up to date version of Node.js, lacking the type stripping features. If this is the case, you'll want to run TypeScript in watch mode (yarn tsc --watch) in one terminal, and then execute the compiled JS code directly: npde --env-file=./.env dist/server.js.

Production

In production, we want to minimize overhead. To do so, we compile the TypeScript to JavaScript up front by running the yarn build command. Then we ship the resulting files (including the dist/ folder) to production and we run the npm run prod command which executes the compiled code directly.