Skip to content

Commit

Permalink
feat(gatsby): first pass at API functions docs (#31066) (#31329)
Browse files Browse the repository at this point in the history
* feat(gatsby): first pass at API functions docs

* Lots of docs tweaks most notably rename API Functions to just Functions

* tweak

* tweaks

* add cors example to integration test

* Just call it one thing

* Update routing.md

(cherry picked from commit 88ca620)

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
  • Loading branch information
GatsbyJS Bot and KyleAMathews committed May 7, 2021
1 parent d54510f commit 6a6f46b
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 10 deletions.
109 changes: 109 additions & 0 deletions docs/docs/how-to/functions/getting-started.md
@@ -0,0 +1,109 @@
---
title: Getting Started
---

Gatsby Functions help you build [Express-like](https://expressjs.com/) backends without running servers.

## Hello World

JavaScript and Typescript files in `src/api/*` are mapped to function routes like files in `src/pages/*` become pages.

For example, the following Function is run when you visit the URL `/api/hello-world`

```js:title=src/api/hello-world.js
export default function handler(req, res) {
res.status(200).json({ hello: `world` })
}
```

A Function file must export a single function that takes two parameters:

- req: An instance of [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage) with some [automatically parsed data](/docs/how-to/functions/getting-started/#common-data-formats-are-automatically-parsed)
- res: An instance of [http.ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) with some [helper functions](/docs/how-to/functions/middleware-and-helpers/#res-helpers)

Dynamic routing is supported for creating REST-ful APIs and other uses cases

- `/api/users` => `src/api/users/index.js`
- `/api/users/23` => `src/api/users/[id].js`

[Learn more about dynamic routes](/docs/how-to/functions/routing#dynamic-routing)

## Typescript

Functions can be written in JavaScript or Typescript.

```ts:title=src/api/typescript.ts
import { GatsbyFunctionRequest, GatsbyFunctionResponse } from "gatsby"

export default function handler(
req: GatsbyFunctionRequest,
res: GatsbyFunctionResponse
) {
res.send(`I am TYPESCRIPT`)
}
```

## Common data formats are automatically parsed

Query strings and common body content types are automatically parsed and available at `req.query` and `req.body`

Read more about [supported data formats](/docs/how-to/functions/middleware-and-helpers).

```js:title=src/api/contact-form.js
export default function contactFormHandler(req, res) {
// "req.body" contains the data from a contact form
}
```

## Respond to HTTP Methods

Sometimes you want to respond differently to GETs vs. POSTs or only respond
to one method.

```js:title=src/api/method-example.js
export default function handler(req, res) {
if (req.method === `POST`) {
res.send(`I am POST`)
} else {
// Handle other methods or return error
}
}
```

## Environment variables

Site [environment variables](/docs/how-to/local-development/environment-variables) are used to pass secrets and environment-specific configuration to Functions.

```js:title=src/api/users/[id].js
import fetch from "node-fetch"

export default async function postNewPersonHandler(req, res) {
// POST data to an authenticated API
const url = "https://example.com/people"

const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.CLIENT_TOKEN}`,
}

const data = {
name: req.body.name,
occupation: req.body.occupation,
age: req.body.age,
}

try {
const result = await fetch(url, {
method: "POST",
headers: headers,
body: data,
}).then(res => {
return res.json()
})

res.json(result)
} catch (error) {
res.status(500).send(error)
}
}
```
54 changes: 54 additions & 0 deletions docs/docs/how-to/functions/middleware-and-helpers.md
@@ -0,0 +1,54 @@
---
title: Middleware and Helpers
---

Gatsby Functions provides an [Express-like](https://expressjs.com/) architecture that simplifies building
Node.js APIs. We include a number of middlewares to parse common request data as well as response helpers.

## Data formats

We parse commonly used data types. Available on the `req` object:

- Cookies at `req.cookies`
- URL Queries (e.g. `api/foo?query=foo`) at `req.query`
- Form parameters and data at `req.body`
- JSON POST bodies at `req.body`

## Response helpers

- `res.send(body)` — returns the response. The `body` can be a `string`, `object`, or `buffer`
- `res.json(body)` — returns a JSON response. The `body` can be any value that can be seralized with `JSON.stringify()`
- `res.status(statusCode)` — set the [HTTP status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) for the response. Defaults to `200`.
- `res.redirect([statusCode], url)` — Returns a redirect to a URL. Optionally set the statusCode which defaults to `302`.

## Custom middleware

Custom Connect/Express middleware are supported.

An example of how to add CORS support to route:

```js:title=src/api/cors.js
import Cors from "cors"

const cors = Cors()

export default async function corsHandler(req, res) {
// Run Cors middleware and handle errors.
await new Promise((resolve, reject) => {
cors(req, res, result => {
if (result instanceof Error) {
reject(result)
}

resolve(result)
})
})

res.json(`Hi from Gatsby Functions`)
}
```

## Custom body parsing

This is not yet supported. [Add a comment in the discussion if this is an
important use case for you](https://github.com/gatsbyjs/gatsby/discussions/30735).
50 changes: 50 additions & 0 deletions docs/docs/how-to/functions/routing.md
@@ -0,0 +1,50 @@
---
title: Routing
---

Function routing shares the same syntax as [page routing](/docs/reference/routing/file-system-route-api/).

## Static Routing

Both top-level and nested routes are supported.

- `src/api/top-level.js` => `/api/top-level`
- `src/api/directory/foo.js` => `/api/directory/foo`

`index.js` files are routed at their directory path e.g. `src/api/users/index.js` => `/api/users`

## Dynamic Routing

_Note: Dynamic Routing is not yet supported on Gatsby Cloud. Expect it in another few weeks. It will work locally._

### Param routes

Use square brackets (`[ ]`) in the file path to mark dynamic segments of the URL.

So to create an Function for fetching user information by `userId`:

```js:title=src/api/users/[id].js
export default async function handler(req, res) {
const userId = req.params.id

// Fetch user
const user = await getUser(userId)

res.json(user)
}
```

Dynamic routes share syntax with [client-only routes](/docs/reference/routing/file-system-route-api/#creating-client-only-routes).

### Splat routes

Gatsby also supports splat (or wildcard) routes, which are routes that will match anything after the splat. These are less common, but still have use cases.

```js:title=src/api/foo/[...].js
export default function handler(req, res) {
const params = req.params[`0`].split(`/`)

// `src/api/foo/1/2 // params[0] === `1`
// params[1] === `2`
}
```
18 changes: 18 additions & 0 deletions integration-tests/functions/src/api/cors.js
@@ -0,0 +1,18 @@
import Cors from "cors"

const cors = Cors()

export default async function corsHandler(req, res) {
// Run Cors middleware and handle errors.
await new Promise((resolve, reject) => {
cors(req, res, result => {
if (result instanceof Error) {
reject(result)
}

resolve(result)
})
})

res.json(`Hi from Gatsby Functions`)
}
6 changes: 3 additions & 3 deletions integration-tests/functions/src/api/i-am-typescript.ts
@@ -1,8 +1,8 @@
import { GatsbyAPIFunctionResponse, GatsbyAPIFunctionRequest } from "gatsby"
import { GatsbyFunctionResponse, GatsbyFunctionRequest } from "gatsby"

export default function topLevel(
req: GatsbyAPIFunctionRequest,
res: GatsbyAPIFunctionResponse
req: GatsbyFunctionRequest,
res: GatsbyFunctionResponse
) {
if (req.method === `GET`) {
res.send(`I am typescript`)
Expand Down
9 changes: 9 additions & 0 deletions integration-tests/functions/test-helpers.js
Expand Up @@ -220,6 +220,15 @@ export function runTests(env, host) {
})
})

describe(`functions can have custom middleware`, () => {
test(`normal`, async () => {
const result = await fetch(`${host}/api/cors`)

const headers = Object.fromEntries(result.headers)
expect(headers[`access-control-allow-origin`]).toEqual(`*`)
})
})

// TODO figure out why this gets into endless loops
// describe.only(`hot reloading`, () => {
// const fixturesDir = path.join(__dirname, `fixtures`)
Expand Down
14 changes: 7 additions & 7 deletions packages/gatsby/index.d.ts
Expand Up @@ -1529,9 +1529,9 @@ export interface PluginOptionsSchemaArgs {
type Send<T> = (body: T) => void

/**
* Gatsby API Function route response
* Gatsby Function route response
*/
export interface GatsbyAPIFunctionResponse extends ServerResponse {
export interface GatsbyFunctionResponse extends ServerResponse {
/**
* Send `any` data in response
*/
Expand All @@ -1543,15 +1543,15 @@ export interface GatsbyAPIFunctionResponse extends ServerResponse {
/**
* Set the HTTP status code of the response
*/
status: (statusCode: number) => GatsbyAPIFunctionResponse<T>
redirect(url: string): GatsbyAPIFunctionResponse<T>
redirect(status: number, url: string): GatsbyAPIFunctionResponse<T>
status: (statusCode: number) => GatsbyFunctionResponse<T>
redirect(url: string): GatsbyFunctionResponse<T>
redirect(status: number, url: string): GatsbyFunctionResponse<T>
}

/**
* Gatsby API function route request
* Gatsby function route request
*/
export interface GatsbyAPIFunctionRequest extends IncomingMessage {
export interface GatsbyFunctionRequest extends IncomingMessage {
/**
* Object of values from URL query parameters (after the ? in the URL)
*/
Expand Down

0 comments on commit 6a6f46b

Please sign in to comment.