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.
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
| Field | Type | Default | Description |
|---|---|---|---|
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 returns | The response |
|---|---|
A Response | Sent 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 GioEventStream | A 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 undefined | 204 with no body. |
| Any other value | 200, 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 anAllowheader listing what it does export (withHEADwhenGETis there).OPTIONScannot be exported and gets the same405. - Errors.
notFound()answers404{"error":"Not Found"}.req.json()orreq.formData()on a body sent with another content type answers415, and a body that does not parse (JSON or form)400(MalformedBodyError). Any other thrown error answers500{"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-Controlto them: set your own when you want one. Aredirect()is the exception: as from an action, it getsprivate, no-cacheunless its headers set one, so no CDN stores a per-user guard's301or308. - Before your code runs, the Rust server applies guards, redirects, rewrites, rate limits, CSRF protection (a cross-site
POST,PUT,PATCHorDELETEgets403) and the body limit ([server] max_body_bytes, 2 MiB by default). - Import errors. A
route.tsthat throws while it is imported answers500to every method, and closes WebSocket connections with1011, until it is fixed.gio routesmarks 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
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
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
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.
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(withparams,query, lowercasedheaders,cookiesand the body), not a webRequest, and it may return plain values. Headers are lowercased. export const revalidateand other page exports do nothing in aroute.ts.route.tsfiles are imported once at startup, and their methods are read then. In development a change restarts the worker, which imports them again.gio exportskips route handlers: a static site has no server to run them.
Related
- Route Handlers - the guide (request object, cookies, SSE, streaming).
- HTTP methods and
wsHandler - Request errors,
notFound - WebSockets, page.tsx
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Matched 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.5 | HTTP method handlers (GET, POST, PUT, PATCH, DELETE), 405 with Allow, SSE from GET. |
v0.1.0-beta.1 | Introduced for wsHandler exports; route.ts or route.js. |