Skip to main content

Node.js ESM Reference

This reference covers the full contract for Node.js Http functions (Node 20, 22, 24). All three runtimes share the same event, response, and configuration interface — code written for one works on any of them without changes.

info

This guide applies to Node.js Http runtimes (Node 20/22/24) which use ESM format (index.mjs, export const handler). For Express runtimes (Node 18/20), refer to the Express handler syntax.


The Event Object

Every incoming HTTP request is converted into an event object passed to your handler.

FieldTypeDescription
event.httpMethodstring"GET", "POST", "PUT", "DELETE", "PATCH", etc.
event.pathstringURL path, e.g. "/users/42"
event.headersobjectAll request headers as key-value pairs (lowercase keys)
event.multiValueHeadersobjectSame headers but each value is an array
event.queryStringParametersobject|nullQuery params as strings — ?a=1&a=2 gives a:"2" (last wins). null if no query.
event.multiValueQueryStringParametersobject|nullQuery params as arrays — ?a=1&a=2 gives a:["1","2"]. null if no query.
event.bodystring|nullRequest body as a string. JSON/text = UTF-8. Binary = base64-encoded. null if empty.
event.isBase64Encodedbooleantrue when body is base64 (binary uploads). false for JSON/text.
event.rawBodyBufferRaw bytes of the request body. Use this for multipart/file parsing.
event.requestContext.stagestring"prod", "staging", or "local"
event.requestContext.identity.sourceIpstringClient IP address
event.requestContext.identity.userAgentstringClient User-Agent header

Reading request data

export const handler = async (event) => {
const body = JSON.parse(event.body);
const page = event.queryStringParameters?.page ?? '1';
const match = event.path.match(/^\/users\/([a-z0-9]+)$/i);
const userId = match?.[1];
const ip = event.requestContext.identity.sourceIp;
};

The Response Object

Your handler must return a plain object. Only statusCode is required.

return {
statusCode: 200, // required — integer 100–599
headers: { // optional — single value per header
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
},
multiValueHeaders: { // optional — multiple values per header
'Set-Cookie': ['session=abc', 'theme=dark'],
},
body: JSON.stringify({ ok: true }), // string, object, or null
isBase64Encoded: false, // set true when sending binary files
};

Common response patterns

GoalReturn value
JSON response{ statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data }) }
Plain text{ statusCode: 200, headers: { 'Content-Type': 'text/plain' }, body: 'Hello' }
Send a file{ statusCode: 200, headers: { 'Content-Type': 'image/png', 'Content-Disposition': 'inline; filename="photo.png"' }, body: buffer.toString('base64'), isBase64Encoded: true }
Set cookies{ statusCode: 200, multiValueHeaders: { 'Set-Cookie': ['session=abc; HttpOnly', 'theme=dark; Max-Age=86400'] }, body: JSON.stringify({ ok: true }) }
Redirect{ statusCode: 302, headers: { Location: 'https://example.com' }, body: '' }
No content{ statusCode: 204, body: null }
Error{ statusCode: 400, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'email is required' }) }

init() — Configure Your Function

init() is optional. Export it to configure CORS and body handling at startup. If you don't export it, sensible defaults apply.

warning

init() must be synchronous. No async, no await, no database connections — configuration only.

export const init = () => ({
cors: {
origins: ['*'], // which domains can call your function from a browser
credentials: false, // allow cookies and Authorization headers
},
body: {
json: true, // accept application/json (default: true)
urlencoded: false, // accept application/x-www-form-urlencoded
contentTypes: [], // additional MIME types — strings or RegExp
maxBytes: 10 * 1024 * 1024, // max body size (default 10MB, max 100MB)
},
});

Defaults when init() is not exported

SettingDefaultMeaning
cors.origins["*"]Any domain can call your function
cors.credentialsfalseCookies and auth headers not forwarded
body.jsontrueJSON requests accepted
body.urlencodedfalseForm submissions rejected with 415
body.contentTypes[]No additional content types accepted
body.maxBytes10 MBRequests with larger bodies get 413

CORS Configuration

CORS controls which browser domains can call your function. It does not affect server-to-server calls (curl, Postman, backend services).

Allow any origin (default)

cors: { origins: ['*'] }

Restrict to specific domains

cors: {
origins: ['https://myapp.com', 'https://staging.myapp.com'],
}

Allow cookies and Authorization headers

cors: {
origins: ['https://myapp.com'], // must NOT be wildcard
credentials: true,
}
warning

credentials: true with origins: ["*"] is blocked by policy. You must list specific domains.

Dynamic origins from environment variable

cors: { origins: (process.env.CORS_ORIGINS || '*').split(',') }

Set CORS_ORIGINS=https://myapp.com,https://staging.myapp.com in Function Configuration.


Body Handling

By default only JSON is accepted. Any other content type is rejected with 415 Unsupported Media Type before your handler is called.

Supported content types

Content TypeMIME TypeHow to enable
JSONapplication/jsonbody.json: true (default)
Form submissionapplication/x-www-form-urlencodedbody.urlencoded: true
File uploadmultipart/form-datacontentTypes: [/^multipart\/form-data/]
XMLtext/xmlcontentTypes: ["text/xml"]
Plain texttext/plaincontentTypes: ["text/plain"]
Images (all)image/*contentTypes: [/^image\//]
PDFapplication/pdfcontentTypes: ["application/pdf"]
Binaryapplication/octet-streamcontentTypes: ["application/octet-stream"]

Examples

JSON + form submissions

export const init = () => ({
body: { json: true, urlencoded: true },
});

export const handler = async (event) => {
const form = Object.fromEntries(new URLSearchParams(event.body));
// form.email, form.name, etc.
};

File uploads

export const init = () => ({
body: {
contentTypes: [/^multipart\/form-data/],
maxBytes: 50 * 1024 * 1024, // 50MB
},
});

// Use event.rawBody (not event.body) for multipart parsing
import Busboy from 'busboy';
import { Readable } from 'stream';

export const handler = async (event) => {
const busboy = Busboy({ headers: event.headers });
const readable = new Readable();
readable.push(event.rawBody);
readable.push(null);
readable.pipe(busboy);
};

Images

export const init = () => ({
body: { contentTypes: [/^image\//], maxBytes: 5 * 1024 * 1024 },
});
// event.isBase64Encoded is true, event.rawBody has raw bytes

How body is encoded per content type

Content Typeevent.bodyevent.isBase64Encoded
application/jsonUTF-8 stringfalse
text/plain, text/xmlUTF-8 stringfalse
x-www-form-urlencodedUTF-8 stringfalse
multipart/form-database64 stringtrue
image/*, application/pdfbase64 stringtrue
No body (GET, DELETE)nullfalse
tip

For binary content (multipart, images, PDF), always use event.rawBody instead of event.body.


Error Handling

What the platform handles automatically

ConditionStatusHandler called?
Body exceeds maxBytes413No
Content-Type not in allowed list415No
Handler returns wrong shape500Yes — but response was invalid
Handler throws an error500Yes — error logged, generic message sent to client

Error messages from your handler are never sent to clients. Only generic messages are returned. Your errors are logged.

What you handle

export const handler = async (event) => {
try {
const body = JSON.parse(event.body);
if (!body.email) {
return { statusCode: 400, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'email required' }) };
}
// your logic here
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
} catch (err) {
console.error(err); // logged for your team
return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Internal Server Error' }) };
}
};

init() Quick Reference

Goalinit() config
Use defaultsDon't export init() — JSON + 10MB + open CORS
Lock CORScors: { origins: ['https://app.com'] }
Enable cookiescors: { origins: ['https://app.com'], credentials: true }
Accept formsbody: { urlencoded: true }
File uploadsbody: { contentTypes: [/^multipart\/form-data/], maxBytes: 50 * 1024 * 1024 }
XML APIbody: { json: false, contentTypes: ['text/xml'] }
Imagesbody: { contentTypes: [/^image\//] }
Strict size limitbody: { maxBytes: 1024 * 1024 } — 413 on >1MB