diff --git a/examples/with-netlify/icon.png b/examples/with-netlify/icon.png
new file mode 100644
index 0000000..a6b9fd3
Binary files /dev/null and b/examples/with-netlify/icon.png differ
diff --git a/examples/with-netlify/netlify.toml b/examples/with-netlify/netlify.toml
new file mode 100644
index 0000000..df5b7d3
--- /dev/null
+++ b/examples/with-netlify/netlify.toml
@@ -0,0 +1,12 @@
+[functions]
+external_node_modules = ["@stackpress/ingest"]
+
+[build]
+command = "yarn build"
+environment = { NODE_VERSION = "20" }
+functions = "src"
+
+[[redirects]]
+from = "/*"
+to = "/.netlify/functions/handler"
+status = 200
\ No newline at end of file
diff --git a/examples/with-netlify/package.json b/examples/with-netlify/package.json
new file mode 100644
index 0000000..91a66a9
--- /dev/null
+++ b/examples/with-netlify/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "ingest-with-fetch",
+ "version": "1.0.0",
+ "description": "A simple boilerplate for using Ingest with fetch API.",
+ "private": true,
+ "scripts": {
+ "build": "tsc",
+ "dev": "ts-node src/server.ts"
+ },
+ "dependencies": {
+ "@netlify/functions": "^3.0.0",
+ "@stackpress/ingest": "0.3.27"
+ },
+ "devDependencies": {
+ "@types/node": "^22.13.1",
+ "ts-node": "10.9.2",
+ "typescript": "^4.9.5"
+ }
+}
diff --git a/examples/with-netlify/src/handler/handler.mts b/examples/with-netlify/src/handler/handler.mts
new file mode 100755
index 0000000..40dc25a
--- /dev/null
+++ b/examples/with-netlify/src/handler/handler.mts
@@ -0,0 +1,28 @@
+import { server } from '@stackpress/ingest/fetch';
+import pages from '../routes/pages';
+import user from '../routes/user';
+import tests from '../routes/tests';
+import hooks from '../routes/hooks';
+
+export async function handler(event: any, context: any) {
+ const app = server();
+ await app.bootstrap();
+
+ app.use(pages).use(user).use(hooks).use(tests);
+
+ const request = new Request(event.rawUrl, {
+ method: event.httpMethod,
+ headers: event.headers,
+ });
+
+ const response = await app.handle(request, undefined);
+
+ return {
+ statusCode: response?.status,
+ headers: response?.headers
+ ? Object.fromEntries(response.headers.entries())
+ : {},
+ body: await response?.text(),
+ isBase64Encoded: false,
+ };
+}
diff --git a/examples/with-netlify/src/routes/hooks.ts b/examples/with-netlify/src/routes/hooks.ts
new file mode 100644
index 0000000..b862a85
--- /dev/null
+++ b/examples/with-netlify/src/routes/hooks.ts
@@ -0,0 +1,56 @@
+import type { ResponseStatus } from '@stackpress/lib/dist/types';
+import { getStatus } from '@stackpress/lib/dist/Status';
+import { Exception } from '@stackpress/ingest';
+import { router } from '@stackpress/ingest/fetch';
+
+const route = router();
+
+/**
+ * Error handlers
+ */
+route.get('/catch', function ErrorResponse(req, res) {
+ try {
+ throw Exception.for('Not implemented');
+ } catch (e) {
+ const error = e as Exception;
+ const status = getStatus(error.code) as ResponseStatus;
+ res.setError({
+ code: status.code,
+ status: status.status,
+ error: error.message,
+ stack: error.trace()
+ });
+ }
+});
+
+/**
+ * Error handlers
+ */
+route.get('/error', function ErrorResponse(req, res) {
+ throw Exception.for('Not implemented');
+});
+
+/**
+ * 404 handler
+ */
+route.get('/**', function NotFound(req, res) {
+ if (!res.code && !res.status && !res.sent) {
+ //send the response
+ res.setHTML('Not Found');
+ }
+});
+
+route.on('error', function Error(req, res) {
+ const html = [ `
${res.error}
` ];
+ const stack = res.stack?.map((log, i) => {
+ const { line, char } = log;
+ const method = log.method.replace(//g, '>');
+ const file = log.file.replace(//g, '>');
+ return `#${i + 1} ${method} - ${file}:${line}:${char}`;
+ }) || [];
+ html.push(`${stack.join('
')}`);
+
+ res.setHTML(html.join('
'));
+});
+
+export default route;
\ No newline at end of file
diff --git a/examples/with-netlify/src/routes/pages.ts b/examples/with-netlify/src/routes/pages.ts
new file mode 100644
index 0000000..582c94e
--- /dev/null
+++ b/examples/with-netlify/src/routes/pages.ts
@@ -0,0 +1,39 @@
+import { router } from '@stackpress/ingest/fetch';
+
+const template = `
+
+
+
+ Login
+
+
+ Login
+
+
+
+`;
+
+const route = router();
+
+/**
+ * Home page
+ */
+route.get('/', function HomePage(req, res) {
+ res.setHTML('Hello, World!
from Netlify
');
+});
+
+/**
+ * Login page
+ */
+route.get('/login', function Login(req, res) {
+ //send the response
+ res.setHTML(template.trim());
+});
+
+export default route;
diff --git a/examples/with-netlify/src/routes/tests.ts b/examples/with-netlify/src/routes/tests.ts
new file mode 100644
index 0000000..7393637
--- /dev/null
+++ b/examples/with-netlify/src/routes/tests.ts
@@ -0,0 +1,81 @@
+import fs from 'fs';
+import path from 'path';
+
+import { router } from '@stackpress/ingest/fetch';
+
+const template = `
+
+
+
+ SSE
+
+
+
+
+
+
+`;
+
+const route = router();
+
+/**
+ * Redirect test
+ */
+route.get('/redirect', function Redirect(req, res) {
+ res.redirect('/user');
+});
+
+/**
+ * Static file test
+ */
+route.get('/icon.png', function Icon(req, res) {
+ if (res.code || res.status || res.body) return;
+ const file = path.resolve(process.cwd(), 'icon.png');
+ if (fs.existsSync(file)) {
+ res.setBody('image/png', fs.createReadStream(file));
+ }
+});
+
+/**
+ * Stream template for SSE test
+ */
+route.get('/stream', function Stream(req, res) {
+ //send the response
+ res.setHTML(template.trim());
+});
+
+/**
+ * SSE test
+ */
+route.get('/__sse__', function SSE(req, res) {
+ res.headers
+ .set('Cache-Control', 'no-cache')
+ .set('Content-Encoding', 'none')
+ .set('Connection', 'keep-alive')
+ .set('Access-Control-Allow-Origin', '*');
+
+ let timerId: any;
+ const msg = new TextEncoder().encode('data: hello\r\n\r\n');
+ res.setBody('text/event-stream', new ReadableStream({
+ start(controller) {
+ timerId = setInterval(() => {
+ controller.enqueue(msg);
+ }, 2500);
+ },
+ cancel() {
+ if (typeof timerId === 'number') {
+ clearInterval(timerId);
+ }
+ },
+ }));
+});
+
+export default route;
\ No newline at end of file
diff --git a/examples/with-netlify/src/routes/user.ts b/examples/with-netlify/src/routes/user.ts
new file mode 100644
index 0000000..149fbd6
--- /dev/null
+++ b/examples/with-netlify/src/routes/user.ts
@@ -0,0 +1,102 @@
+import { router } from '@stackpress/ingest/fetch';
+
+const route = router();
+
+let id = 0;
+
+/**
+ * Example user API search
+ */
+route.get('/user', function UserSearch(req, res) {
+ //get filters
+ //const filters = req.query.get>('filter');
+ //maybe get from database?
+ const results = [
+ {
+ id: 1,
+ name: 'John Doe',
+ age: 21,
+ created: new Date().toISOString()
+ },
+ {
+ id: 2,
+ name: 'Jane Doe',
+ age: 30,
+ created: new Date().toISOString()
+ }
+ ];
+ //send the response
+ res.setRows(results, 100);
+});
+
+/**
+ * Example user API create (POST)
+ * Need to use Postman to see this...
+ */
+route.post('/user', function UserCreate(req, res) {
+ //get form body
+ const form = req.data();
+ //maybe insert into database?
+ const results = { ...form, id: ++id, created: new Date().toISOString() };
+ //send the response
+ res.setResults(results);
+});
+
+/**
+ * Example user API detail
+ */
+route.get('/user/:id', function UserDetail(req, res) {
+ //get params
+ const id = parseInt(req.data('id') || '');
+ if (!id) {
+ res.setError('ID is required');
+ return;
+ }
+ //maybe get from database?
+ const results = {
+ id: id,
+ name: 'John Doe',
+ age: 21,
+ created: new Date().toISOString()
+ };
+ //send the response
+ res.setResults(results);
+});
+route.put('/user/:id', function UserUpdate(req, res) {
+ //get params
+ const id = parseInt(req.data('id') || '');
+ if (!id) {
+ res.setError('ID is required');
+ return;
+ }
+ //get form body
+ const form = req.post();
+ //maybe insert into database?
+ const results = { ...form, id, created: new Date().toISOString() };
+ //send the response
+ res.setResults(results);
+});
+
+/**
+ * Example user API delete (DELETE)
+ * Need to use Postman to see this...
+ */
+route.delete('/user/:id', function UserRemove(req, res) {
+ //get params
+ const id = parseInt(req.data('id') || '');
+ if (!id) {
+ res.setError('ID is required');
+ return;
+ }
+ //maybe get from database?
+ const results = {
+ id: 1,
+ name: 'John Doe',
+ age: 21,
+ created: new Date().toISOString()
+ };
+ //send the response
+ res.setResults(results);
+});
+
+export default route;
\ No newline at end of file
diff --git a/examples/with-netlify/tsconfig.json b/examples/with-netlify/tsconfig.json
new file mode 100644
index 0000000..aff717f
--- /dev/null
+++ b/examples/with-netlify/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "esModuleInterop": true,
+ "lib": ["es2021", "es7", "es6", "dom"],
+ "module": "commonjs",
+ "noUnusedLocals": true,
+ "outDir": "./dist/",
+ "preserveConstEnums": true,
+ "resolveJsonModule": true,
+ "removeComments": true,
+ "sourceMap": false,
+ "strict": true,
+ "target": "es6",
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*.ts", "src/server.mts"],
+ "exclude": ["dist", "node_modules"]
+}