Saltar al contenido principal
Version: 1.x

GraphQL

GraphQL is a query language for APIs. Unlike traditional REST APIs, GraphQL APIs have only one endpoint to which requests are sent. The content of the request describes all the operations to be performed and the data to be returned in the response. Many resources can be retrieved in a single request and the client gets exactly the properties it asks for.

Example of request

{
project(name: "GraphQL") {
tagline
}
}

Example of response

{
"data": {
"project": {
"tagline": "A query language for APIs"
}
}
}

The below document assumes that you have a basic knowledge of GraphQL.

To use GraphQL with FoalTS, you need to install the packages graphql and @foal/graphql. The first one is maintained by the GraphQL community and parses and resolves queries. The second is specific to FoalTS and allows you to configure a controller compatible with common GraphQL clients (graphql-request, Apollo Client, etc), load type definitions from separate files or handle errors thrown in resolvers.

npm install graphql @foal/graphql

Due to a specificity of the graphql library, you must also modify your tsconfig.json as follows:

{
"compilerOptions": {
...
"lib": [
...
"ESNext.AsyncIterable"
]
}
...
}

Basic Usage

The main component of the package is the abstract GraphQLController. Inheriting this class allows you to create a controller that is compatible with common GraphQL clients (graphql-request, Apollo Client, etc) or any client that follows the HTTP specification defined here.

Here is an example on how to use it with a simple schema and resolver.

app.controller.ts

export class AppController {
subControllers = [
controller('/graphql', ApiController)
]
}

api.controller.ts

import { GraphQLController } from '@foal/graphql';
import { buildSchema } from 'graphql';

const schema = buildSchema(`
type Query {
hello: String
}
`);

const root = {
hello: () => {
return 'Hello world!';
},
};

export class ApiController extends GraphQLController {
schema = schema;
resolvers = root;
}

And here is an example of what your client code might look like:

import { request } from 'graphql-request';

const data = await request('/graphql', '{ hello }');
// data equals "{ hello: 'Hello world!' }"

Alternatively, if you have several strings that define your GraphQL types, you can use the schemaFromTypeDefs function to build the schema.

import { GraphQLController, schemaFromTypeDefs } from '@foal/graphql';

const source1 = `
type Query {
me: User
}
`;
const source2 = `
type User {
id: ID
name: String
}
`;

// ...

export class ApiController extends GraphQLController {
schema = schemaFromTypeDefs(source1, source2);
// ...
}

Using Separate Files for Type Definitions

If you want to specify type definitions in separate files, you can use the schemaFromTypeGlob function for this.

Example

src/
'- app/
'- controllers/
|- query.graphql
|- user.graphql
'- api.controller.ts

query.graphql

type Query {
me: User
}

user.graphql

type User {
id: ID
name: String
}

api.controller.ts

import { GraphQLController, schemaFromTypeGlob } from '@foal/graphql';
import { join } from 'path';

export class ApiController extends GraphQLController {
schema = schemaFromTypeGlob(join(__dirname, '**/*.graphql'));
// ...
}

Note that for this to work, you must copy the graphql files during the build. To do this, you can update some lines of your package.json as follows:

{
...
"scripts": {
...
"build:app": "copy-cli \"src/**/*.html\" build && copy-cli \"src/**/*.graphql\" build && tsc -p tsconfig.app.json",
...
"build:test": "copy-cli \"src/**/*.html\" build && copy-cli \"src/**/*.graphql\" && tsc -p tsconfig.test.json",
...
"build:e2e": "copy-cli \"src/**/*.html\" build && copy-cli \"src/**/*.graphql\" && tsc -p tsconfig.e2e.json"
...
}
}

Alternatively, if you want to specify only specific files instead of using a glob pattern, you can call schemaFromTypePaths.

import { GraphQLController, schemaFromTypePaths } from '@foal/graphql';
import { join } from 'path';

export class ApiController extends GraphQLController {
schema = schemaFromTypePaths(
join(__dirname, './query.graphql'),
join(__dirname, './user.graphql')
);
// ...
}

Using a Service for the Root Resolvers

Root resolvers can also be grouped into a service in order to benefit from all the advantages offered by services (dependency injection, etc.). All you have to do is add the @dependency decorator as you would with any service.

api.controller.ts

import { dependency } from '@foal/core';
import { GraphQLController } from '@foal/graphql';
import { RootResolverService } from '../services';

// ...

export class ApiController extends GraphQLController {
schema = // ...

@dependency
resolvers: RootResolverService;
}

root-resolver.service.ts

export class RootResolverService {

hello() {
return 'Hello world!';
}

}

GraphQL Playground

Next releases of FoalTS will include support for GraphiQL.

Error Handling - Masking & Logging Errors

By default, GraphQL returns all errors thrown (or rejected) in the resolvers. However, this behavior is often not desired in production as it could cause sensitive information to leak from the server.

In comparison with REST APIs, when the configuration key settings.debug does not equal true (production case), details of the errors thrown in controllers are not returned to the client. Only a 500 - Internal Server Error error is sent back.

In a similar way, FoalTS provides two utilities formatError and @FormatError for your GraphQL APIs to log and mask errors. When settings.debug is true, the errors are converted into a new one whose unique message is Internal Server Error.

Example of GraphQL response in production

{
"data": { "user": null },
"errors": [
{
"locations": [ { "column": 2, "line": 1 } ],
"message": "Internal Server Error",
"path": [ "user" ]
}
]
}

Here are examples on how to use formatError and @FormatError.

function user() {
// ...
}

const resolvers = {
user: formatError(user)
}
class ResolverService {
@FormatError()
user() {
// ...
}
}

Note that formatError and @FormatError make your functions become asynchronous. This means that any value returned by the function is now a resolved promise of this value, and any errors thrown in the function is converted into a rejected promise. This only has an impact on unit testing as you may need to preceed your function calls by the keyword await.

formatError and @FormatError also accept an optional parameter to override its default behavior. It is a function that takes the error thrown or rejected in the resolver and return the error that must be sent to the client. It may be asynchronous or synchronous.

By default, this function is:

function maskAndLogError(err: any): any {
console.log(err);

if (Config.get('settings.debug')) {
return err;
}

return new Error('Internal Server Error');
}

But we can also imagine other implementations such as:

import { reportErrorTo3rdPartyService } from 'somewhere';

async function maskAndLogError(err: any): Promise<any> {
console.log(err);

try {
await reportErrorTo3rdPartyService(err);
} catch (error) {}

if (err instanceof MyCustomError) {
return err;
}

if (Config.get('settings.debug')) {
return err;
}

return new Error('Internal Server Error');
}

Authentication & Authorization

The below code is an example of managing authentication and authorization with a GraphQL controller.

api.controller.ts

import { GraphQLController } from '@foal/graphql';
import { JWTRequired } from '@foal/jwt';
import { fetchUser } from '@foal/typeorm';
import { buildSchema } from 'graphql';

import { User } from '../entities';

const schema = buildSchema(`
type Query {
hello: String
}
`);

const root = {
hello: (_, context) => {
if (!context.user.isAdmin) {
return null;
}
return 'Hello world!';
},
};

@JWTRequired({ user: fetchUser(User) })
export class ApiController extends GraphQLController {
schema = schema;
resolvers = root;
}

Using TypeGraphQL

TypeGraphQL is a library that allows you to create GraphQL schemas and resolvers with TypeScript classes and decorators.

You can use TypeGraphQL by simply calling its buildSchema function.

Example

import { GraphQLController } from '@foal/graphql';
import { buildSchema, Field, ObjectType, Query, Resolver } from 'type-graphql';

@ObjectType()
class Recipe {
@Field()
title: string;
}

@Resolver(Recipe)
class RecipeResolver {

@Query(returns => Recipe)
async recipe() {
return {
title: 'foobar'
};
}

}

export class ApiController extends GraphQLController {
schema = buildSchema({
resolvers: [ RecipeResolver ]
});
}

Advanced

Override the Resolver Context

The GraphQL context that is passed to the resolvers is by default the request context. This behavior can be changed by overriding the getResolverContext method.

import { Context } from '@foal/core';
import { GraphQLController } from '@foal/graphql';

export class ApiController extends GraphQLController {
// ...

getResolverContext(ctx: Context) {
return { user: ctx.user };
}
}