API Routes


Unlike Next.js, your

api/ folder should be a sibling of pages/ instead of being nested inside. But pages/api is still supported for compatibility with Next.js.

Any file inside an

api/ folder are accessible at a URL corresponding to its path inside api/. So app/projects/api/webhook.ts will be at localhost:3000/api/webhook.

For example, the following API route

app/api/hello.js handles a json response:

export default (req, res) => {
res.statusCode = 200
res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({name: "John Doe"}))

For an API route to work, you need to export as default a function (a.k.a request handler), which then receives the following parameters:

  • req: An instance of http.IncomingMessage, plus some pre-built middlewares you can see here
  • res: An instance of http.ServerResponse, plus some helper functions you can see here

To handle different HTTP methods in an API route, you can use

req.method in your request handler, like so:

export default (req, res) => {
if (req.method === "POST") {
// Process a POST request
} else {
// Handle any other HTTP method

To fetch API endpoints, take a look into any of the examples at the start of this section.

API Routes

do not specify CORS headers, meaning they are same-origin only by default. You can customize such behavior by wrapping the request handler with the cors middleware.

API Routes do not increase your client-side bundle size. They are server-side only bundles.

Authentication and Session Context

You can use the

getSessionContext function to get the session of the user. Here is an example using session in a API route app/api/customRoute.tsx:

import {getSessionContext} from "@blitzjs/server"
export default async function customRoute(req, res) {
const session = await getSessionContext(req, res)
console.log("User ID:", session.userId)
res.statusCode = 200
res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({userId: session.userId}))

This is called in the frontend like this:

import {getAntiCSRFToken} from "blitz"
const antiCSRFToken = getAntiCSRFToken()
const response = await window.fetch("/api/customRoute", {
credentials: "include",
headers: {
"anti-csrf": antiCSRFToken,

Read more about Session Management

Dynamic API Routes

Dynamic API routes follow the same file naming rules used for


For example, the API route

app/api/post/[pid].js has the following code:

export default (req, res) => {
const {
query: {pid},
} = req
res.end(`Post: ${pid}`)

Now, a request to

/api/post/abc will respond with the text: Post: abc.

Index routes and Dynamic API routes

A very common RESTful pattern is to set up routes like this:

  • GET api/posts/ - gets a list of posts, probably paginated
  • GET api/posts/12345 - gets post id 12345

We can model this in two ways:

  • Option 1:

    • /api/posts.js
    • /api/posts/[postId].js
  • Option 2:

    • /api/posts/index.js
    • /api/posts/[postId].js

Both are equivalent.

Catch all API routes

API Routes can be extended to catch all paths by adding three dots (

...) inside the brackets. For example:

  • app/api/post/[...slug].js matches /api/post/a, but also /api/post/a/b, /api/post/a/b/c and so on.

Note: You can use names other than

slug, such as: [...param]

Matched parameters will be passed as a query parameter (

slug in the example) to the api handler, and it will always be an array, so, the path /api/post/a will have the following query object:

{"slug": ["a"]}

And in the case of

/api/post/a/b, and any other matching path, new parameters will be added to the array, like so:

{"slug": ["a", "b"]}

An API route for

app/api/post/[...slug].js could look like this:

export default (req, res) => {
const {
query: {slug},
} = req
res.end(`Post: ${slug.join(", ")}`)

Now, a request to

/api/post/a/b/c will respond with the text: Post: a, b, c.

Optional catch all API routes

Catch all routes can be made optional by including the parameter in double brackets (


For example,

app/api/post/[[...slug]].js will match /api/post, /api/post/a, /api/post/a/b, and so on.


query objects are as follows:

{ } // GET `/api/post` (empty object)
{ "slug": ["a"] } // `GET /api/post/a` (single-element array)
{ "slug": ["a", "b"] } // `GET /api/post/a/b` (multi-element array)


  • Predefined API routes take precedence over dynamic API routes, and dynamic API routes over catch all API routes. Take a look at the following examples:
    • app/api/post/create.js - Will match /api/post/create
    • app/api/post/[pid].js - Will match /api/post/1, /api/post/abc, etc. But not /api/post/create
    • app/api/post/[...slug].js - Will match /api/post/1/2, /api/post/a/b/c, etc. But not /api/post/create, /api/post/abc

Response Helpers

The response (

res) includes a set of Express.js-like methods to improve the developer experience and increase the speed of creating new API endpoints, take a look at the following example:

export default (req, res) => {
res.status(200).json({name: "Blitz.js"})

The included helpers are:

  • res.status(code) - A function to set the status code. code must be a valid HTTP status code
  • res.json(json) - Sends a JSON response. json must be a valid JSON object
  • res.send(body) - Sends the HTTP response. body can be a string, an object or a Buffer

Default Middlewares

API routes provide built in middlewares which parse the incoming request (

req). Those middlewares are:

  • req.cookies - An object containing the cookies sent by the request. Defaults to {}
  • req.query - An object containing the query string. Defaults to {}
  • req.body - An object containing the body parsed by content-type, or null if no body was sent

Custom config

Every API route can export a

config object to change the default configs, which are the following:

export const config = {
api: {
bodyParser: {
sizeLimit: "1mb",


api object includes all configs available for API routes.

bodyParser enables body parsing, you can disable it if you want to consume it as a Stream:

export const config = {
api: {
bodyParser: false,

bodyParser.sizeLimit is the maximum size allowed for the parsed body, in any format supported by bytes, like so:

export const config = {
api: {
bodyParser: {
sizeLimit: "500kb",

externalResolver is an explicit flag that tells the server that this route is being handled by an external resolver like express or connect. Enabling this option disables warnings for unresolved requests.

export const config = {
api: {
externalResolver: true,

Connect/Express middleware support

You can also use

Connect compatible middleware.

For example,

configuring CORS for your API endpoint can be done leveraging the cors package.

First, install


npm i cors
# or
yarn add cors

Now, let's add

cors to the API route:

import Cors from "cors"
// Initializing the cors middleware
const cors = Cors({
methods: ["GET", "HEAD"],
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) {
return reject(result)
return resolve(result)
async function handler(req, res) {
// Run the middleware
await runMiddleware(req, res, cors)
// Rest of the API logic
res.json({message: "Hello Everyone!"})
export default handler
Idea for improving this page?Edit it on GitHub
Bytes Newsletter