In some cases the simple example is not enough, and you want to add more customization to your server. This is where the lazy loaded overrides come in. You can override any part of the server by providing a function that returns a promise that resolves to the override object. This is useful when you want to add custom logic to your server, like adding a custom queue, or adding a custom converter.
Be careful if you use the edge runtime (either in a function or by using the external middleware), we do 2 compilations of the open-next.config.ts
, one for node and one for the edge runtime. If you're using some custom overrides, you likely want to add
edgeExternals: ['./customWrapper', './anyOtherOverrideUsed']
to your open-next.config.ts
to avoid the edge runtime to try to compile overrides that are not compatible with the edge runtime.
Custom converter
Sometimes you might want to modify the object received by OpenNext. For example Config.YOUR_SECRET_KEY
from sst cannot be used in the middleware, so you might want to add it to the headers. This is where the custom converter comes in. You can add a custom converter to modify the object before it is passed to OpenNext.
You'll still have to use a fallback value during dev as this is not used by the dev server.
// customConverter.ts
import converter from 'open-next/converters/aws-apigw-v2.js'
import type { Converter } from 'open-next/types/open-next'
import { Config } from 'sst/node/Config'
const mySecretKey = Config.YOUR_SECRET_KEY
export default {
convertFrom: async (event) => {
const result = await converter.convertFrom(event)
return {
...result,
headers: {
...result.headers,
'inserted-in-converter': '1',
'my-super-secret-key': mySecretKey
}
}
},
convertTo : async (intResult) => {
const result = await converter.convertTo(intResult)
return {
...result,
headers: {
...result.headers,
'x-converter-end': '1'
}
}
},
name: 'custom-apigw-v2'
} as Converter
// open-next.config.ts
import type { OpenNextConfig } from 'open-next/types/open-next'
const config = {
default: {
override: {
converter: () => import('./customConverter').then((mod) => mod.default)
}
}
} as OpenNextConfig
Custom wrapper
Here we provide a few examples for some custom wrapper.
Define a global to use node in the middleware
// customWrapper.ts
import defaultWrapper from 'open-next/wrappers/aws-lambda.js'
//Here you can define some globals
declare global {
var myApi: () => Promise<number>;
}
globalThis.myApi = async () => {
const crypto = await import('crypto')
return {
nb: crypto.randomInt(0, 100)
}
}
export default defaultWrapper
// open-next.config.ts
import type { OpenNextConfig } from 'open-next/types/open-next'
const config = {
default: {
override: {
wrapper: () => import('./customWrapper').then((mod) => mod.default)
}
}
} as OpenNextConfig
export default config
But since Next dev server runs in a fake edge runtime and that the global is defined only for deployment, you'll have to mock the global in your middleware.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Here you need to mock the global if not present
// One way to avoid issues with different implementation would be to create an api endpoint
// that uses the exact same logic as the global you defined earlier,
// and that is only available during development i.e. /api/dev/myApi
if(!globalThis.myApi) {
globalThis.myApi = async () => {
return await fetch('http://localhost:3000/api/dev/myApi').then(res => res.json())
}
}
export function middleware(request: NextRequest) {
// You can also send an error in the api endpoint itself
// Or you could add all the dev endpoint in their own lambda
// that you do not deploy in production
if(request.nextUrl.pathname.startsWith('/api/dev') && process.env.NODE_ENV === 'production') {
return NextResponse('This route is only available in development',{
status: 500,
})
}
// Now you can use Node.js in your middleware
const {nb} = await myApi()
// ... your code here
}
Use middy.js with the wrapper
// customWrapper.ts
import streamingWrapper from 'open-next/wrappers/aws-lambda.js'
import {WrapperHandler} from 'open-next/types/open-next'
import middy from '@middy/core'
import httpSecurityHeaders from '@middy/http-security-headers'
const handler : WrapperHandler = async (handler, converter) => {
const defaultHandler = await streamingWrapper.wrapper(handler, converter)
return middy()
.use(httpSecurityHeaders())
.handler(defaultHandler)
}
export default {
wrapper: handler,
name: "custom-aws-lambda",
supportStreaming: false,
};
// open-next.config.ts
import type { OpenNextConfig } from 'open-next/types/open-next'
const config = {
default: {
override: {
wrapper: () => import('./customWrapper').then((mod) => mod.default)
}
}
} as OpenNextConfig
export default config
Preload some routes during warmer event
In this example the custom wrapper is used to preload some important routes before the first request. This is useful if you have some routes that are slow on coldstart (Next lazily load the routes only when they are needed) and you want to preload them before the first request. This is also useful if you want to add some custom logic to the server, like adding a custom header to the response.
WARNING: This one is not properly tested. It's just an example of what you could do. You should test it properly before using it in production. Also preloading too many routes is probably a bad idea.
// customWrapper.ts
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
} from "aws-lambda";
import type { StreamCreator } from "open-next/http/openNextResponse";
import { Writable } from "node:stream";
import { WarmerEvent, WarmerResponse } from "open-next/adapters/warmer-function";
import type { WrapperHandler } from "open-next/types/open-next";
type AwsLambdaEvent =
| APIGatewayProxyEventV2
| WarmerEvent;
type AwsLambdaReturn =
| APIGatewayProxyResultV2
| WarmerResponse;
const serverId = Math.random().toPrecision(5).toString()
let isPreloaded = false
function formatWarmerResponse(event: WarmerEvent) {
return new Promise<WarmerResponse>((resolve) => {
setTimeout(() => {
resolve({ serverId, type: "warmer" } satisfies WarmerResponse);
}, event.delay);
});
}
const handler: WrapperHandler =
async (handler, converter) =>
async (event: AwsLambdaEvent): Promise<AwsLambdaReturn> => {
console.log('custom wrapper')
// Handle warmer event
if ("type" in event) {
if(!isPreloaded) {
// You could preload every route you want here
// Be careful, while the route is preloading the lambda cannot process other requests
await handler({
type: 'core',
url: '/myRoute',
method: 'GET',
headers: {},
query: {},
rawPath: '/myRoute',
cookies: {},
remoteAddress: ''
});
isPreloaded = true
}
return formatWarmerResponse(event);
}
const internalEvent = await converter.convertFrom(event);
internalEvent.headers['inserted-in-wrapper'] = 'hello from wrapper'
//This is a workaround, there is an issue in node that causes node to crash silently if the OpenNextNodeResponse stream is not consumed
//This does not happen everytime, it's probably caused by suspended component in ssr (either via <Suspense> or loading.tsx)
//Everyone that wish to create their own wrapper without a StreamCreator should implement this workaround
//This is not necessary if the underlying handler does not use OpenNextNodeResponse (At the moment, OpenNextNodeResponse is used by the node runtime servers and the image server)
const fakeStream: StreamCreator = {
writeHeaders: () => {
return new Writable({
write: (_chunk, _encoding, callback) => {
callback();
},
});
},
onFinish: () => {
// Do nothing
},
};
const response = await handler(internalEvent, fakeStream);
response.headers['x-wrapper'] = 'hi'
return converter.convertTo(response, event);
};
export default {
wrapper: handler,
name: "custom-aws-lambda",
supportStreaming: false,
};
// open-next.config.ts
import type { OpenNextConfig } from 'open-next/types/open-next'
const config = {
default: {
override: {
wrapper: () => import('./customWrapper').then((mod) => mod.default)
}
}
} as OpenNextConfig
Custom Incremental cache
TODO
Custom queue
TODO
Custom Tag cache
TODO
Custom Origin Resolver
TODO
Custom Image Loader
TODO
Custom Warmer Invoke
TODO