skip to content

In AnalogJS, how do you build a newsletter-signup API route under src/server/routes/api, and what does it need at deploy time?

level: seniorimportance: should knowfreq 20%

answer

  1. files become HTTP endpoints
  2. an h3 event handler per file
  3. a method suffix in the file name
  4. the default /api prefix
  5. no server output, no endpoint

basics

~10 s

In AnalogJS, a file like src/server/routes/api/newsletter.post.ts that default-exports an h3 defineEventHandler becomes POST /api/newsletter. Nitro compiles it into Analog's server output, so it exists only where that server runs, never in a static-only build.

solid answer

~40 s

API routes are file-based too: files under `src/server/routes/api` are served under the default `/api` prefix (changeable with the `apiPrefix` option), and a `.post.ts` or `.get.ts` suffix limits a file to one HTTP method. Each file default-exports an h3 handler, `defineEventHandler(async (event) => ...)`, reads input with `readBody`, `getQuery` or `getRouterParam`, and returns a value that h3 serialises, objects as JSON. An uncaught error becomes a 500; `throw createError({ statusCode: 400, statusMessage })` sends a deliberate client error. Nitro compiles these handlers into the same server that does SSR, built for the Node preset by default, and the Vite dev server runs them in development. The deploy-time catch: `static: true` prerenders pages and builds no server, so a POST endpoint like the signup does not exist in production.

code

ts · 18 lines
ts
// src/server/routes/api/newsletter.post.ts  ->  POST /api/newsletter
import { createError, defineEventHandler, readBody } from 'h3';

import { subscribe } from '../../../lib/mailing-list'; // your server-only module

type SignupBody = { email?: string };

export default defineEventHandler(async (event) => {
  const body = await readBody<SignupBody>(event);
  const email = body?.email?.trim().toLowerCase();

  if (!email || !email.includes('@')) {
    throw createError({ statusCode: 400, statusMessage: 'A valid email is required' });
  }

  await subscribe(email); // uses process.env secrets on the server
  return { subscribed: true };
});

go deeper

for a junior

Recall the folder, src/server/routes/api, the /api prefix, and that each file default-exports defineEventHandler from h3.

for a middle

Explain how the file path maps to a URL and method, how to read the body, query and params with h3 helpers, and how createError sets a status.

for a senior

Show deploy-time judgment: API routes exist only in the Nitro server output, static: true drops them, only GET data can be prerendered, and a form server action may fit a single-page form better.

for a principal

Decide whether a marketing site should run a server at all: a signup endpoint forces one, so weigh a static deploy with the signup handled elsewhere against keeping Analog's server.

## Where API routes live AnalogJS serves HTTP endpoints from **`src/server/routes/api`**. These files are not Angular code: they run on the server inside **Nitro**, the server toolkit Analog uses to build its server output, and each one default-exports an event handler from **h3**, the small HTTP library Nitro is built on. Routes are exposed under the default **`/api` prefix**; the `apiPrefix` option of the `analog()` plugin changes it. The file path decides the URL and, optionally, the HTTP method: | File under `src/server/routes/api` | Answers | URL | |---|---|---| | `newsletter.post.ts` | POST only | `/api/newsletter` | | `products.ts` | any method | `/api/products` | | `products/[sku].get.ts` | GET only | `/api/products/:sku`, read with `getRouterParam(event, 'sku')` | | `rss.xml.ts` | any method | `/api/rss.xml` | | `[...].ts` | any method | fallback for otherwise unmatched `/api` paths | Method suffixes (`.get`, `.post`, `.put`, `.delete` and so on) are the clean way to keep a signup endpoint from answering a GET. ## Writing the signup handler A handler receives the h3 `event` and uses h3 helpers on it: - `readBody(event)` parses a JSON or URL-encoded form body. - `getQuery(event)` reads the query string; `getRouterParam(event, name)` reads a bracketed path segment. - `setHeader(event, name, value)` sets a response header, for example `content-type` for XML. - The return value becomes the response body: an object is sent as JSON, a string as text. Secrets such as the mailing-list API key are read from environment variables inside the handler and never reach the browser, because this code is only in the server output. ## Errors and status codes 1. A handler that returns normally answers **200 OK**. 2. An **uncaught error** becomes **500 Internal Server Error**. 3. To send another status, **throw `createError({ statusCode, statusMessage })`**: 400 for an invalid email, 409 if the address is already subscribed, and so on. Validate the body before using it: `readBody` returns whatever the client sent, typed only by the generic you choose. ## Calling it from the Angular app In the browser, `HttpClient` can post to the relative URL `/api/newsletter`. Server-side code that calls API routes during rendering needs absolute URLs; Analog's `requestContextInterceptor`, registered last in `withInterceptors`, rewrites relative URLs on the server and during prerendering. For a plain HTML form there is an Analog-specific alternative: a **form server action**. Export `action` from the page's `newsletter.server.ts`, add the `FormAction` directive from `@analogjs/router` to the page, and return `json(...)`, `fail(422, errors)` or `redirect('/')` from `@analogjs/router/server/actions`. That handler is tied to one page; an API route is a URL any caller can use. ## In development During development the Vite dev server starts a Nitro dev server and mounts it under the API prefix, so `POST /api/newsletter` works from the running app without a separate backend process. The same handler file is then compiled into the production server. This convenience is also a trap: the dev server always has Nitro, so a build option that removes the server, such as `static: true`, only shows its effect after a production build. ## What it needs at deploy time - **A running server.** Analog's production build writes the client files to `dist/analog/public` and, with the default Node preset, a server started with `node dist/analog/server/index.mjs` (port from `PORT` or `NITRO_PORT`, default 3000). - **The right preset.** `BUILD_PRESET` or `nitro.preset` in `analog()` switches Nitro's output format for a different runtime; the handlers stay the same. - **Not `static: true`.** That option prerenders the listed pages and **skips building the server**. The site deploys to a static host, but `POST /api/newsletter` has nothing behind it and fails. Choose a server build, or move the signup elsewhere. - **Prerendering only suits GET data.** Listing a route such as `/api/rss.xml` in `prerender.routes` writes it as a static file (`dist/analog/public/api/rss.xml`). A POST endpoint cannot be prerendered, because its answer depends on each request. ## Pitfalls - Putting the file in `src/app/pages`: that folder holds Angular pages; an API route there is just ignored or becomes a page. - Forgetting the method suffix, so a crawler's GET to the signup URL reaches the handler. - Letting `readBody` output flow straight into the mailing-list client without validation. - Testing only with the dev server, where Nitro always runs, and discovering the missing endpoint after a static deploy.

  • In AnalogJS, when would you use a form server action instead of an API route for the newsletter signup?
    When the signup is an HTML form on one page. Export `action` from `newsletter.server.ts`, add `FormAction` from `@analogjs/router` to the page, and return `json`, `fail` or `redirect` from `@analogjs/router/server/actions`; the directive posts the form data and emits `onSuccess` or `onError`. An API route fits when several pages, scripts or other clients call the same URL.
  • In AnalogJS, can an API route be served from a static-only build?
    Only as prerendered GET output. Listing `/api/rss.xml` in `prerender.routes` writes a static file under `dist/analog/public/api/`. Anything that depends on the request, such as a POST signup or a filtered query, needs the Nitro server, which `static: true` does not build.
  • In AnalogJS, how does an API route return a status other than 200 or 500?
    Throw `createError({ statusCode: 400, statusMessage: 'A valid email is required' })` from h3 inside the handler. Returning normally sends 200 and an uncaught exception sends 500, so any deliberate client error must be thrown this way.

saying these in an interview costs you the question

  • Analog API routes are Angular services, so they can inject components' providers.
  • An uncaught error in an Analog API route is returned as a 400.
  • static: true still ships the API routes as serverless functions.
  • A POST API route can be prerendered by listing it in prerender.routes.
  • Without a .post suffix, an Analog API route only answers POST requests.