Skip to content

Commit

Permalink
feat: add customize content type parsers for api plugin (#10573)
Browse files Browse the repository at this point in the history
Alterations to #10449

---------

Co-authored-by: scott1028 <mic1028002@gmail.com>
Co-authored-by: Tobbe Lundberg <tobbe@tlundberg.com>
Co-authored-by: David Thyresson <dthyresson@gmail.com>
  • Loading branch information
4 people committed Jun 6, 2024
1 parent bd92504 commit d921d66
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 42 deletions.
3 changes: 3 additions & 0 deletions .changesets/10573.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- feat: add customize content type parsers for api plugin (#10573) by @Josh-Walker-GM

This change adds a new `configureApiServer` option to the `createServer` function used within the server file. This fixes an issue where it was not possible to configure the API server - such as adding custom content parsers or adding compression. Updated docs on how to use this new functionality can currently be found [here](https://redwoodjs.com/docs/canary/docker#using-the-server-file).
2 changes: 1 addition & 1 deletion __fixtures__/test-project/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"postcss": "^8.4.37",
"postcss-loader": "^8.1.1",
"prettier-plugin-tailwindcss": "0.4.1",
"tailwindcss": "^3.4.3"
"tailwindcss": "^3.4.4"
}
}
72 changes: 43 additions & 29 deletions docs/docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,8 +559,9 @@ Not updating the command will not completely configure the GraphQL Server and no

### 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 @@ -576,18 +577,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 @@ -605,21 +598,23 @@ Then register them with the appropriate config:
```ts title="api/src/server.ts"
const server = await createServer({
logger,
async configureApiServer(server) {
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 @@ -641,19 +636,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){
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
7 changes: 7 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 @@ -54,6 +56,11 @@ export async function redwoodFastifyAPI(
}
}

// Run users custom server configuration function
if (redwoodOptions.configureServer) {
await redwoodOptions.configureServer(fastify as Server)
}

fastify.all(`${redwoodOptions.apiRootPath}:routeName`, lambdaRequestHandler)
fastify.all(`${redwoodOptions.apiRootPath}:routeName/*`, lambdaRequestHandler)
await loadFunctionsFromDist({
Expand Down

0 comments on commit d921d66

Please sign in to comment.