|
| 1 | +# Node-Boot Netlify Sample |
| 2 | + |
| 3 | +A sample project that shows how to build, run locally and deploy a |
| 4 | +[Node-Boot](https://github.com/nodejs-boot/node-boot) application as a **Netlify Function**, using |
| 5 | +the [`@nodeboot/netlify-server`](../../serverless/netlify-server) package. |
| 6 | + |
| 7 | +It demonstrates: |
| 8 | + |
| 9 | +- Dependency Injection (`@EnableDI`) using explicit injection tokens |
| 10 | +- Request validation with `class-validator` (`@EnableValidations`) |
| 11 | +- Authorization (`@EnableAuthorization`, `@Authorized()`) |
| 12 | +- Controllers, services, middleware and a custom error handler |
| 13 | +- Runtime configuration without relying on filesystem discovery (`appConfig` object instead of |
| 14 | + `app-config.yaml`) |
| 15 | +- Local development with the Netlify CLI (`netlify dev`) or a plain Node.js smoke test |
| 16 | + (`pnpm run invoke:local`) |
| 17 | +- Deployment with `netlify deploy` |
| 18 | + |
| 19 | +## Project layout |
| 20 | + |
| 21 | +``` |
| 22 | +netlify/ |
| 23 | +└── functions/ |
| 24 | + └── api.ts # Netlify Function entry point (catch-all handling everything under /api/*) |
| 25 | +netlify.toml # Redirect rule routing /api/* to the function above |
| 26 | +src/ |
| 27 | +├── app.ts # NodeBootApplication bootstrapped on NetlifyServer |
| 28 | +├── app-config.ts # Runtime application config, as a plain object |
| 29 | +├── local-invoke.ts # Smoke-test script (calls the handler directly, no Netlify CLI needed) |
| 30 | +├── controllers/ # HTTP controllers |
| 31 | +├── services/ # Business logic (in-memory user store) |
| 32 | +├── models/ # DTOs / validation models |
| 33 | +├── middlewares/ # Logging middleware + custom error handler |
| 34 | +└── auth/ # Authorization/CurrentUser resolvers |
| 35 | +``` |
| 36 | + |
| 37 | +## How it works |
| 38 | + |
| 39 | +Unlike the Express/Koa/Fastify samples, this application never "listens" on a port. Netlify |
| 40 | +invokes `netlify/functions/api.ts` as a Function for every request routed to it. |
| 41 | +`NodeBoot.run(NetlifyServer, appConfig)` bootstraps the DI container, controllers, middleware and |
| 42 | +routes exactly once, and `NetlifyServer#getHandler()` returns a function of shape |
| 43 | +`(event: HandlerEvent, context: HandlerContext) => Promise<HandlerResponse>`: |
| 44 | + |
| 45 | +```typescript |
| 46 | +// netlify/functions/api.ts |
| 47 | +let netlifyHandler: NetlifyHandler | null = null; |
| 48 | + |
| 49 | +export const handler: NetlifyHandler = async (event, context) => { |
| 50 | + if (!netlifyHandler) { |
| 51 | + const app = await new NetlifySampleApp().start(); |
| 52 | + netlifyHandler = (app.server as NetlifyServer).getHandler(); |
| 53 | + } |
| 54 | + return netlifyHandler(event, context); |
| 55 | +}; |
| 56 | +``` |
| 57 | + |
| 58 | +`netlifyHandler` is cached at module scope so the DI container, controllers and routes are only |
| 59 | +rebuilt on a cold start; warm invocations of the same Function instance reuse the same instance. |
| 60 | + |
| 61 | +Because NodeBoot's `routePrefix` is configured as `/api` (see `src/app-config.ts`), the redirect |
| 62 | +rule in `netlify.toml` rewrites every request under `/api/*` to `/.netlify/functions/api`, lining |
| 63 | +up Netlify's routing with NodeBoot's internal router: |
| 64 | + |
| 65 | +```toml |
| 66 | +[[redirects]] |
| 67 | + from = "/api/*" |
| 68 | + to = "/.netlify/functions/api" |
| 69 | + status = 200 |
| 70 | +``` |
| 71 | + |
| 72 | +## Running locally |
| 73 | + |
| 74 | +### Option 1: Plain Node.js smoke test (no Netlify CLI/account needed) |
| 75 | + |
| 76 | +```bash |
| 77 | +pnpm install |
| 78 | +pnpm run build |
| 79 | +pnpm run invoke:local |
| 80 | +``` |
| 81 | + |
| 82 | +This calls the exact same handler function deployed to Netlify directly, building |
| 83 | +`HandlerEvent`/`HandlerContext` objects by hand, and fires a few sample requests against it: |
| 84 | + |
| 85 | +``` |
| 86 | +GET /api/hello -> 200 Hello, from Node-Boot running on Netlify! |
| 87 | +POST /api/users -> 201 {"id":"...","email":"ada@example.com","name":"Ada Lovelace"} |
| 88 | +GET /api/users -> 200 [{"id":"...","email":"ada@example.com","name":"Ada Lovelace"}] |
| 89 | +``` |
| 90 | + |
| 91 | +### Option 2: `netlify dev` (requires the Netlify CLI) |
| 92 | + |
| 93 | +```bash |
| 94 | +pnpm run dev |
| 95 | +``` |
| 96 | + |
| 97 | +This runs the actual `netlify dev` local server, which mimics Netlify's production routing/runtime |
| 98 | +more closely than the plain smoke test above (redirects, headers, etc.). |
| 99 | + |
| 100 | +```bash |
| 101 | +curl http://localhost:8888/api/hello |
| 102 | +curl http://localhost:8888/api/users |
| 103 | +curl -X POST http://localhost:8888/api/users \ |
| 104 | + -H "Content-Type: application/json" -H "Authorization: Bearer token" \ |
| 105 | + -d '{"name":"Ada Lovelace","email":"ada@example.com"}' |
| 106 | +``` |
| 107 | + |
| 108 | +## Deploying |
| 109 | + |
| 110 | +```bash |
| 111 | +npx netlify login |
| 112 | +pnpm run deploy |
| 113 | +``` |
| 114 | + |
| 115 | +`pnpm run deploy` runs `netlify deploy --prod`, which uploads the project and lets Netlify's |
| 116 | +`esbuild`-based function bundler compile and bundle `netlify/functions/api.ts` (and everything it |
| 117 | +statically imports) automatically. |
| 118 | + |
| 119 | +The first deploy will prompt you to link the directory to a new or existing Netlify site. |
| 120 | +Subsequent deploys reuse that link (stored in the git-ignored `.netlify/` directory). |
| 121 | + |
| 122 | +## Netlify vs. traditional Node.js servers: what's different, and why |
| 123 | + |
| 124 | +Netlify Functions run in a real Node.js runtime (AWS Lambda under the hood), so most things "just |
| 125 | +work". A few points are still worth calling out: |
| 126 | + |
| 127 | +1. **No long-lived process / no component-scanning.** `@EnableComponentScan()` reads compiled |
| 128 | + files from `dist/` at runtime (`fs.readdirSync`). Netlify Functions only ship the subset of |
| 129 | + files statically traced from the function's entry point (via esbuild), so relying on directory |
| 130 | + scanning for beans not reachable through static imports is unreliable. This sample explicitly |
| 131 | + imports every controller/service/middleware in `src/app.ts` for their decorator side effects |
| 132 | + instead, guaranteeing they're always included in the bundled function. |
| 133 | + |
| 134 | +2. **No filesystem-based `app-config.yaml` discovery.** `@nodeboot/config` normally walks up the |
| 135 | + directory tree from `process.cwd()` looking for `app-config.yaml`. A Function's working |
| 136 | + directory is not guaranteed to match the project root, so this sample passes configuration as a |
| 137 | + plain object (`src/app-config.ts`) straight into `NodeBoot.run(NetlifyServer, appConfig)` |
| 138 | + instead, guaranteeing identical configuration locally and once deployed. |
| 139 | + |
| 140 | +3. **Routing is event-based, not request/response based.** Like AWS Lambda's |
| 141 | + `APIGatewayProxyEvent`/`APIGatewayProxyResult`, Netlify's Node.js runtime hands the handler an |
| 142 | + immutable `HandlerEvent` and expects an immutable `HandlerResponse` value back. |
| 143 | + `NetlifyDriver` builds and returns the final response value instead of writing directly to a |
| 144 | + mutable response object. |
| 145 | + |
| 146 | +4. **`find-my-way`'s trailing-slash routes.** Controller index routes build routes with a |
| 147 | + trailing slash (e.g. `@Controller("/hello")` + `@Get("/")` → `/hello/`), but real request URLs |
| 148 | + typically omit it (e.g. `/api/hello`). `@nodeboot/netlify-server` configures its `find-my-way` |
| 149 | + router with `ignoreTrailingSlash: true` so both forms match. |
| 150 | + |
| 151 | +None of the above are specific to this sample — they're general considerations for running any |
| 152 | +Node-Boot application on Netlify Functions. |
0 commit comments