GioJSdocs
On this page

route.ts

A file whose exported GET, POST, PUT, PATCH and DELETE functions answer HTTP requests to its folder's URL, plus an optional WebSocket handler.

app/api/posts/route.ts
import type { GioRequest } from '@gio.js/core';

export function GET(req: GioRequest) {
  return { posts: [], page: req.query.page ?? '1' };
}

export async function POST(req: GioRequest) {
  const { title } = req.json<{ title: string }>();
  return Response.json({ created: title }, { status: 201 });
}

Reference

File name and location

route.ts or route.js (.ts wins when both exist), in app/ or any folder below it except private folders. Its URL comes from the folder path like a page's, dynamic segments and route groups included: app/api/items/[id]/route.ts answers /api/items/:id.

Exports

FieldTypeDefaultDescription
GET(req: GioRequest) => unknown-Answers GET, and HEAD (the body is dropped). May return a GioEventStream to send Server-Sent Events.
POST(req: GioRequest) => unknown-Answers POST.
PUT(req: GioRequest) => unknown-Answers PUT.
PATCH(req: GioRequest) => unknown-Answers PATCH.
DELETE(req: GioRequest) => unknown-Answers DELETE.
wsHandler(socket: GioSocket) => void | boolean | Promise<void | boolean>-Accepts WebSocket connections to the same URL. See wsHandler.

Handlers may be async. Type one for its route with RouteHandler<'/api/items/:id'>, which types req.params. Other exports are ignored. The request object and everything a handler can return are described on HTTP methods.

Return values

The handler returnsThe response
A ResponseSent as it is: status, headers, body (a ReadableStream body streams). Without a Content-Type it gets text/plain. A Response from fetch() (return fetch(upstream)) loses the upstream's Content-Encoding and Content-Length: fetch() already decoded the body, and GioJS compresses it again. HTML gets the deployment script injected, so its length is always the server's.
A GioEventStreamA text/event-stream connection, from any method (a browser's EventSource sends GET).
redirect() (returned or thrown)Its status (303 by default) and headers, Location as written (a relative path works), no body.
null or undefined204 with no body.
Any other value200, application/json; charset=utf-8, the value as JSON. A string becomes a JSON string ("hello").

Behavior

  • Other methods. A method the file does not export gets 405, {"error":"Method Not Allowed"} and an Allow header listing what it does export (with HEAD when GET is there). OPTIONS cannot be exported and gets the same 405.
  • Errors. notFound() answers 404 {"error":"Not Found"}. req.json() or req.formData() on a body sent with another content type answers 415, and a body that does not parse (JSON or form) 400 (MalformedBodyError). Any other thrown error answers 500 {"error":"Internal Server Error","digest":"..."}, with the details in the log under that digest.
  • Never cached. Handler responses are not stored or coalesced, and GioJS adds no Cache-Control to them: set your own when you want one. A redirect() is the exception: as from an action, it gets private, no-cache unless its headers set one, so no CDN stores a per-user guard's 301 or 308.
  • Before your code runs, the Rust server applies guards, redirects, rewrites, rate limits, CSRF protection (a cross-site POST, PUT, PATCH or DELETE gets 403) and the body limit ([server] max_body_bytes, 2 MiB by default).
  • Import errors. A route.ts that throws while it is imported answers 500 to every method, and closes WebSocket connections with 1011, until it is fixed. gio routes marks it (failed to load).

Next to a page

A route.ts may sit in the same folder as a page.tsx. The file answers the methods it exports; the page answers GET and HEAD (and POST with an action) unless the route.ts exports them too. A page and a route.ts in different folders that answer the same URLs (for example in two route groups) stop startup with an error naming both files.

Matching

Pages and route.ts files are matched together, the most specific pattern winning segment by segment (see matching order). So a catch-all app/api/[...path]/route.ts never takes a URL that a more specific page or route.ts answers.

Examples

A dynamic route handler

app/api/items/[id]/route.ts
import { notFound } from '@gio.js/core';
import type { RouteHandler } from '@gio.js/core';
import { db } from '../../../../lib/db';

export const GET: RouteHandler<'/api/items/:id'> = async (req) => {
  const item = await db.items.find(req.params.id);
  if (item === undefined) notFound();
  return item;
};

export const DELETE: RouteHandler<'/api/items/:id'> = async (req) => {
  await db.items.remove(req.params.id);
  return null; // 204
};

Reject a malformed JSON body

app/api/comments/route.ts
import { isUnsupportedMediaTypeError } from '@gio.js/core';
import type { GioRequest } from '@gio.js/core';

export function POST(req: GioRequest) {
  let input: unknown;
  try {
    input = req.json();
  } catch (error) {
    if (isUnsupportedMediaTypeError(error)) throw error; // GioJS answers 415
    // A body that is not JSON, or no body at all.
    return Response.json({ error: 'send a JSON body' }, { status: 400 });
  }
  const text = (input as { text?: unknown } | null)?.text;
  if (typeof text !== 'string') {
    return Response.json({ error: 'text is required' }, { status: 422 });
  }
  return Response.json({ saved: text }, { status: 201 });
}

A response with its own headers

app/api/export/route.ts
export function GET() {
  const csv = 'id,name\n1,Ada\n';
  return new Response(csv, {
    headers: {
      'content-type': 'text/csv; charset=utf-8',
      'content-disposition': 'attachment; filename="users.csv"',
      'cache-control': 'private, max-age=60',
    },
  });
}

A webhook another site posts to

Server-to-server webhooks carry no browser headers, so CSRF protection lets them through. An endpoint that browsers on other sites post to on purpose (an OAuth form_post callback) must be listed in [security.csrf] exempt.

app/api/webhooks/stripe/route.ts
import type { GioRequest } from '@gio.js/core';
import { verifyStripeSignature } from '../../../../lib/stripe';

export async function POST(req: GioRequest) {
  const signature = req.headers['stripe-signature'];
  if (signature === undefined || req.body === null || !verifyStripeSignature(req.body, signature)) {
    return new Response('bad signature', { status: 400 });
  }
  // ... handle the event
  return null;
}

Good to know

  • There is no route.tsx: a route file returns data, not JSX.
  • Unlike Next.js, the handler gets a GioRequest (with params, query, lowercased headers, cookies and the body), not a web Request, and it may return plain values. Headers are lowercased.
  • export const revalidate and other page exports do nothing in a route.ts.
  • route.ts files are imported once at startup, and their methods are read then. In development a change restarts the worker, which imports them again.
  • gio export skips route handlers: a static site has no server to run them.

Version history

VersionChanges
v0.1.0-beta.8Matched with pages by one precedence rule. A file that throws while it is imported answers 500. notFound() answers a JSON 404. req.json() requires a JSON content type (415), and a JSON body that does not parse is a 400 instead of a 500. redirect() works in handlers. RouteHandler type. WebSocket handlers in dynamic folders. return fetch(upstream) no longer forwards an encoding fetch() already decoded, and HTML with its own Content-Length is no longer cut short.
v0.1.0-beta.5HTTP method handlers (GET, POST, PUT, PATCH, DELETE), 405 with Allow, SSE from GET.
v0.1.0-beta.1Introduced for wsHandler exports; route.ts or route.js.