Requests & Responses
Working with requests and responses
Every route receives the request and reply objects from Fastify as props. Everything Fastify offers is available, so there is no extra API to learn.
Reading the request
The most useful properties of the request object:
request.method: the HTTP method, e.g.GETorPOSTrequest.path: the decoded path without query string, e.g./blog/hello(added by Jeasx, see Routes)request.query: the parsed query string as object, e.g.{ page: "2" }for?page=2request.headers: all request headers in lower case, e.g.request.headers["accept-language"]request.body: the parsed request body forPOST,PUTorPATCHrequestsrequest.ip,request.hostnameandrequest.protocol: details about the client and the connection
// src/[search].jsx
export default function ({ request }) {
const term = request.query.q ?? "";
return <p>You searched for: {term}</p>;
}
Query values always arrive as strings (or arrays of strings if a parameter is repeated), so convert and validate them before use, e.g. Number.parseInt(request.query.page) || 1.
Handling HTTP methods
A route answers all HTTP methods. Check request.method to handle them differently, and answer methods you don't support with status 405:
// src/[subscribe].jsx
export default async function ({ request, reply }) {
switch (request.method) {
case "GET":
return <SubscribeForm />;
case "POST":
await saveSubscription(request.body);
return <p>Thanks for subscribing!</p>;
default:
reply.status(405).header("allow", "GET, POST");
return "Method not allowed";
}
}
Static files from public and dist are only served for GET and HEAD, so a POST to the same path reaches your routes.
Forms
Fastify parses JSON bodies out of the box. For classic HTML forms (application/x-www-form-urlencoded), register @fastify/formbody or a small parser in jeasx.config.js (see Configuration). The fields are then available as request.body:
// src/[contact].jsx
export default async function ({ request, reply }) {
const errors = {};
if (request.method === "POST") {
const { email = "", message = "" } = request.body ?? {};
if (!email.includes("@")) errors.email = "Please enter a valid email address.";
if (!message.trim()) errors.message = "Please enter a message.";
if (Object.keys(errors).length === 0) {
await sendMail(email, message);
// Redirect after a successful POST, so a reload doesn't submit the form again.
return reply.redirect("/contact?sent=1", 303);
}
}
return (
<form method="post">
{request.query.sent && <p>Thanks, your message was sent.</p>}
<input type="email" name="email" value={request.body?.email} />
{errors.email && <p class="error">{errors.email}</p>}
<textarea name="message">{request.body?.message}</textarea>
{errors.message && <p class="error">{errors.message}</p>}
<button>Send</button>
</form>
);
}
Values from the request are escaped automatically when you render them, so the form can safely show the submitted input again. For file uploads, use @fastify/multipart. Don't forget CSRF protection if your forms change data for logged-in users (see Security).
What a route can return
The return value of a route becomes the response:
| Return value | Response |
|---|---|
| JSX | Rendered and streamed as HTML |
| String, Buffer or stream | Sent as is, as text/html unless you set another content type |
| Plain object or array | Serialized as JSON by Fastify |
A Response (e.g. from fetch) | Sent with its status, headers and body |
Nothing (undefined) | The next matching route is tried, e.g. a [...path] or [404] route |
Guards are the exception: an object returned by a guard isn't sent, but passed as props to the routes below (see Guards).
Status codes and headers
Set the status and headers on reply before you return the payload. The methods can be chained:
// src/[feed.xml].js
export default async function ({ reply }) {
const posts = await loadPosts();
reply
.status(200)
.type("application/rss+xml; charset=utf-8")
.header("cache-control", "public, max-age=600");
return renderFeed(posts);
}
reply.type() sets the content type. Without it, strings and streams are sent as text/html; charset=utf-8.
Redirects
Return reply.redirect() to send the browser somewhere else. The default status is 302; pass 301 for permanent moves or 303 after a form submission:
// src/[old-page].jsx
export default function ({ reply }) {
return reply.redirect("/new-page", 301);
}
Once a reply is sent (by reply.redirect() or reply.send()), Jeasx stops and doesn't try any other route. Never redirect to a URL taken unchecked from the request (e.g. ?next=https://evil.example), only to paths you allow.
Cookies
Register @fastify/cookie in jeasx.config.js. Then read cookies via request.cookies and set them with reply.setCookie():
// src/[theme].jsx
export default function ({ request, reply }) {
const theme = request.query.theme === "dark" ? "dark" : "light";
reply.setCookie("theme", theme, {
path: "/",
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 365,
});
return reply.redirect("/", 303);
}
A guard is a good place to read cookies (e.g. a session) once and pass the result to all routes via the context or as props.
JSON APIs
Return a plain object or array and Fastify sends it as JSON. Putting the extension into the route name (e.g. [posts.json].js) keeps your URLs self-explanatory:
// src/api/[posts.json].js
export default async function ({ request, reply }) {
if (request.method === "POST") {
const post = await createPost(request.body);
reply.status(201);
return post;
}
return await listPosts();
}
Routes don't have to be JSX. For APIs, feeds or generated files, a plain .js or .ts route that returns an object, a string or a stream is often the simplest choice.