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

feat: add customize content type parsers for api plugin #10573

Merged
merged 16 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 43 additions & 29 deletions docs/docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,9 @@ That means you can swap the `CMD` instruction in the api server stage:

### Configuring the server

There's two ways you can configure the server.
There are three ways you may wish to configure the server.

#### Underlying Fastify server
First, you can configure how the underlying Fastify server is instantiated via the`fastifyServerOptions` passed to the `createServer` function:

```ts title="api/src/server.ts"
Expand All @@ -548,18 +549,10 @@ const server = await createServer({

For the complete list of options, see [Fastify's documentation](https://fastify.dev/docs/latest/Reference/Server/#factory).

Second, you can register Fastify plugins on the server instance:
#### Configure the redwood API plugin
Second, you may want to alter the behavior of redwood's API plugin itself. To do this we provide a `configureApiServer(server)` option where you can do anything you wish to the fastify instance before the API plugin is registered. Two examples are given below.

```ts title="api/src/server.ts"
const server = await createServer({
logger,
})

// highlight-next-line
server.register(myFastifyPlugin)
```

#### Example: Compressing Payloads and Rate Limiting
##### Example: Compressing Payloads and Rate Limiting

Let's say that we want to compress payloads and add rate limiting.
We want to compress payloads only if they're larger than 1KB, preferring deflate to gzip,
Expand All @@ -577,21 +570,23 @@ Then register them with the appropriate config:
```ts title="api/src/server.ts"
const server = await createServer({
logger,
async configureApiServer(server) {
Josh-Walker-GM marked this conversation as resolved.
Show resolved Hide resolved
await server.register(import('@fastify/compress'), {
global: true,
threshold: 1024,
encodings: ['deflate', 'gzip'],
})

await server.register(import('@fastify/rate-limit'), {
max: 100,
timeWindow: '5 minutes',
})
}
})

await server.register(import('@fastify/compress'), {
global: true,
threshold: 1024,
encodings: ['deflate', 'gzip'],
})

await server.register(import('@fastify/rate-limit'), {
max: 100,
timeWindow: '5 minutes',
})
```

#### Example: File Uploads
##### Example: File Uploads

If you try to POST file content to the api server such as images or PDFs, you may see the following error from Fastify:

Expand All @@ -613,19 +608,38 @@ For example, to support image file uploads you'd tell Fastify to allow `/^image\
```ts title="api/src/server.ts"
const server = await createServer({
logger,
})

server.addContentTypeParser(/^image\/.*/, (req, payload, done) => {
payload.on('end', () => {
done()
})
configureApiServer(server){
Josh-Walker-GM marked this conversation as resolved.
Show resolved Hide resolved
server.addContentTypeParser(/^image\/.*/, (req, payload, done) => {
payload.on('end', () => {
done()
})
})
}
})
```

The regular expression (`/^image\/.*/`) above allows all image content or MIME types because [they start with "image"](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types).

Now, when you POST those content types to a function served by the api server, you can access the file content on `event.body`.

#### Additional Fastify plugins
Finally, you can register additional Fastify plugins on the server instance:

```ts title="api/src/server.ts"
const server = await createServer({
logger,
})

// highlight-next-line
server.register(myFastifyPlugin)
```

:::note Fastify encapsulation

Fastify is built around the concept of [encapsulation](https://fastify.dev/docs/latest/Reference/Encapsulation/). It is important to note that redwood's API plugin cannot be mutated after it is registered, see [here](https://fastify.dev/docs/latest/Reference/Plugins/#asyncawait). This is why you must use the `configureApiServer` option to do as shown above.

:::

### The `start` method

Since there's a few different ways to configure the host and port the server listens at, the server instance returned by `createServer` has a special `start` method:
Expand Down
1 change: 1 addition & 0 deletions packages/api-server/src/__tests__/createServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ describe('resolveOptions', () => {

expect(resolvedOptions).toEqual({
apiRootPath: DEFAULT_CREATE_SERVER_OPTIONS.apiRootPath,
configureApiServer: DEFAULT_CREATE_SERVER_OPTIONS.configureApiServer,
fastifyServerOptions: {
requestTimeout:
DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout,
Expand Down
26 changes: 16 additions & 10 deletions packages/api-server/src/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@ import chalk from 'chalk'
import { config } from 'dotenv-defaults'
import fg from 'fast-glob'
import fastify from 'fastify'
import type { FastifyListenOptions, FastifyInstance } from 'fastify'

import type { GlobalContext } from '@redwoodjs/context'
import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store'
import { getConfig, getPaths } from '@redwoodjs/project-config'

import { resolveOptions } from './createServerHelpers'
import type { CreateServerOptions } from './createServerHelpers'
import type {
CreateServerOptions,
Server,
StartOptions,
} from './createServerHelpers'
import { redwoodFastifyAPI } from './plugins/api'

type StartOptions = Omit<FastifyListenOptions, 'port' | 'host'>

interface Server extends FastifyInstance {
start: (options?: StartOptions) => Promise<string>
}

// Load .env files if they haven't already been loaded. This makes importing this file effectful:
//
// ```js
Expand Down Expand Up @@ -51,6 +48,9 @@ if (!process.env.REDWOOD_ENV_FILES_LOADED) {
* const server = await createServer({
* logger,
* apiRootPath: 'api'
* configureApiServer: (server) => {
* // Configure the API server fastify instance, e.g. add content type parsers
* },
* })
*
* // Configure the returned fastify instance:
Expand All @@ -64,8 +64,13 @@ if (!process.env.REDWOOD_ENV_FILES_LOADED) {
* ```
*/
export async function createServer(options: CreateServerOptions = {}) {
const { apiRootPath, fastifyServerOptions, apiPort, apiHost } =
resolveOptions(options)
const {
apiRootPath,
fastifyServerOptions,
configureApiServer,
apiPort,
apiHost,
} = resolveOptions(options)

// Warn about `api/server.config.js`
const serverConfigPath = path.join(
Expand Down Expand Up @@ -114,6 +119,7 @@ export async function createServer(options: CreateServerOptions = {}) {
fastGlobOptions: {
ignore: ['**/dist/functions/graphql.js'],
},
configureServer: configureApiServer,
},
})

Expand Down
22 changes: 20 additions & 2 deletions packages/api-server/src/createServerHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { parseArgs } from 'util'

import type { FastifyServerOptions } from 'fastify'
import type {
FastifyListenOptions,
FastifyServerOptions,
FastifyInstance,
} from 'fastify'

import { coerceRootPath } from '@redwoodjs/fastify-web/dist/helpers'

import { getAPIHost, getAPIPort } from './cliHelpers'

export type StartOptions = Omit<FastifyListenOptions, 'port' | 'host'>

export interface Server extends FastifyInstance {
start: (options?: StartOptions) => Promise<string>
}

export interface CreateServerOptions {
/**
* The prefix for all routes. Defaults to `/`.
Expand All @@ -23,6 +33,11 @@ export interface CreateServerOptions {
*/
fastifyServerOptions?: Omit<FastifyServerOptions, 'logger'>

/**
* Customise the API server fastify plugin before it is registered
*/
configureApiServer?: (server: Server) => void | Promise<void>

/**
* Whether to parse args or not. Defaults to `true`.
*/
Expand All @@ -45,6 +60,7 @@ export const DEFAULT_CREATE_SERVER_OPTIONS: DefaultCreateServerOptions = {
fastifyServerOptions: {
requestTimeout: 15_000,
},
configureApiServer: () => {},
parseArgs: true,
}

Expand Down Expand Up @@ -74,7 +90,9 @@ export function resolveOptions(
DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout,
logger: options.logger ?? DEFAULT_CREATE_SERVER_OPTIONS.logger,
},

configureApiServer:
options.configureApiServer ??
DEFAULT_CREATE_SERVER_OPTIONS.configureApiServer,
apiHost: getAPIHost(),
apiPort: getAPIPort(),
}
Expand Down
6 changes: 6 additions & 0 deletions packages/api-server/src/plugins/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { GlobalContext } from '@redwoodjs/context'
import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store'
import { coerceRootPath } from '@redwoodjs/fastify-web/dist/helpers'

import type { Server } from '../createServerHelpers'
import { loadFastifyConfig } from '../fastify'

import { lambdaRequestHandler, loadFunctionsFromDist } from './lambdaLoader'
Expand All @@ -16,6 +17,7 @@ export interface RedwoodFastifyAPIOptions {
apiRootPath?: string
fastGlobOptions?: FastGlobOptions
loadUserConfig?: boolean
configureServer?: (server: Server) => void | Promise<void>
}
}

Expand Down Expand Up @@ -59,4 +61,8 @@ export async function redwoodFastifyAPI(
await loadFunctionsFromDist({
fastGlobOptions: redwoodOptions.fastGlobOptions,
})

if (redwoodOptions.configureServer) {
await redwoodOptions.configureServer(fastify as Server)
}
}
Loading