🚀Announcing Flightcontrol - Optimized Deployment for Fullstack Blitz.js and Next.js 🚀
Back to Documentation Menu

API Routes

Topics

Jump to a Topic
Info

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:

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

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:

If you want to avoid writing the types BlitzApiRequest and BlitzApiResponse everywhere, you can use BlitzApiHandler:

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

To handle different HTTP methods in an API route, you can use req.method in your request handler, like so:

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

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 getSession function to get the session of the user. Here is an example using session in a API route app/api/customRoute.tsx:

import { getSession, BlitzApiRequest, BlitzApiResponse } from "blitz"

export default async function customRoute(
  req: BlitzApiRequest,
  res: BlitzApiResponse
) {
  const session = await getSession(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 pages.

For example, the API route app/api/post/[pid].js has the following code:

const handler: BlitzApiHandler = (req, res) => {
  const {
    query: { pid },
  } = req

  res.end(`Post: ${pid}`)
}
export default handler

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:

const handler: BlitzApiHandler = (req, res) => {
  const {
    query: { slug },
  } = req

  res.end(`Post: ${slug.join(", ")}`)
}
export default handler

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 ([[...slug]]).

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

The 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)

Caveats

  • 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:

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

The included helpers are:

  • res.status(code) - A function to set the status code. code must be a valid HTTP status code
  • res.json(body) - Sends a JSON response. body must be a valid serialiazable 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",
    },
  },
}

The 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 cors:

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: BlitzApiRequest, res: BlitzApiResponse) {
  // 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.