Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enables 'edge' as a possible runtime for API routes #44045

Merged
merged 2 commits into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 13 additions & 29 deletions docs/advanced-features/react-18/switchable-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,6 @@ Next.js has two **server runtimes** where you can render parts of your applicati

By default, Next.js uses the Node.js runtime. [Middleware](https://nextjs.org/docs/advanced-features/middleware) and [Edge API Routes](https://nextjs.org/docs/api-routes/edge-api-routes) use the Edge runtime.

## Global Runtime Option

To configure the runtime for your whole application, you can set the experimental option `runtime` in your `next.config.js` file:

```js
// next.config.js
module.exports = {
experimental: {
runtime: 'experimental-edge', // 'node.js' (default) | experimental-edge
},
}
```

You can detect which runtime you're using by looking at the `process.env.NEXT_RUNTIME` Environment Variable during runtime, and examining the `options.nextRuntime` variable during compilation.

## Page Runtime Option

On each page, you can optionally export a `runtime` config set to either `'nodejs'` or `'experimental-edge'`:
Expand All @@ -38,22 +23,21 @@ export const config = {
}
```

When both the per-page runtime and global runtime are set, the per-page runtime overrides the global runtime. If the per-page runtime is _not_ set, the global runtime option will be used.

## Runtime Differences

|   | Node (Server) | Node (Serverless) | Edge |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------- | ---------------- |
| [Cold Boot](https://vercel.com/docs/concepts/get-started/compute#cold-and-hot-boots?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) | / | ~250ms | Instant |
| HTTP Streaming | Yes | Yes | Yes |
| IO | All | All | `fetch` |
| Scalability | / | High | Highest |
| Security | Normal | High | High |
| Latency | Normal | Low | Lowest |
| Code Size | / | 50MB | 1MB |
| NPM Packages | All | All | A smaller subset |
|   | Node (Server) | Node (Serverless) | Edge |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------- | -------------------------------------------------------- |
| Name | `nodejs` | `nodejs` | `edge` or `experimental-edge` if using Next.js Rendering |
| [Cold Boot](https://vercel.com/docs/concepts/get-started/compute#cold-and-hot-boots?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) | / | ~250ms | Instant |
| HTTP Streaming | Yes | Yes | Yes |
| IO | All | All | `fetch` |
| Scalability | / | High | Highest |
| Security | Normal | High | High |
| Latency | Normal | Low | Lowest |
| Code Size | / | 50 MB | 4 MB |
| NPM Packages | All | All | A smaller subset |

Next.js' default runtime configuration is good for most use cases, but there’re still many reasons to change to one runtime over the other one.
Next.js' default runtime configuration is good for most use cases, but there are still many reasons to change to one runtime over the other one.

For example, for API routes that rely on native Node.js APIs, they need to run with the Node.js Runtime. However, if an API only uses something like cookie-based authentication, using Middleware and the Edge Runtime will be a better choice due to its lower latency as well as better scalability.

Expand All @@ -63,7 +47,7 @@ For example, for API routes that rely on native Node.js APIs, they need to run w

```typescript
export const config = {
runtime: 'experimental-edge',
runtime: 'edge',
}

export default (req) => new Response('Hello world!')
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/edge-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ You can relax the check to allow specific files with your Middleware or Edge API

```javascript
export const config = {
runtime: 'experimental-edge', // for Edge API Routes only
runtime: 'edge', // for Edge API Routes only
unstable_allowDynamic: [
'/lib/utilities.js', // allows a single file
'/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module
Expand Down
14 changes: 8 additions & 6 deletions docs/api-routes/edge-api-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
description: Edge API Routes enable you to build high performance APIs directly inside your Next.js application.
---

# Edge API Routes (Beta)
# Edge API Routes

Edge API Routes enable you to build high performance APIs with Next.js. Using the [Edge Runtime](/docs/api-reference/edge-runtime.md), they are often faster than Node.js-based API Routes. This performance improvement does come with [constraints](/docs/api-reference/edge-runtime.md#unsupported-apis), like not having access to native Node.js APIs. Instead, Edge API Routes are built on standard Web APIs.

Expand All @@ -14,7 +14,7 @@ Any file inside the folder `pages/api` is mapped to `/api/*` and will be treated

```typescript
export const config = {
runtime: 'experimental-edge',
runtime: 'edge',
}

export default (req) => new Response('Hello world!')
Expand All @@ -26,7 +26,7 @@ export default (req) => new Response('Hello world!')
import type { NextRequest } from 'next/server'

export const config = {
runtime: 'experimental-edge',
runtime: 'edge',
}

export default async function handler(req: NextRequest) {
Expand All @@ -50,7 +50,7 @@ export default async function handler(req: NextRequest) {
import type { NextRequest } from 'next/server'

export const config = {
runtime: 'experimental-edge',
runtime: 'edge',
}

export default async function handler(req: NextRequest) {
Expand All @@ -75,7 +75,7 @@ export default async function handler(req: NextRequest) {
import type { NextRequest } from 'next/server'

export const config = {
runtime: 'experimental-edge',
runtime: 'edge',
}

export default async function handler(req: NextRequest) {
Expand All @@ -91,7 +91,7 @@ export default async function handler(req: NextRequest) {
import { type NextRequest } from 'next/server'

export const config = {
runtime: 'experimental-edge',
runtime: 'edge',
}

export default async function handler(req: NextRequest) {
Expand Down Expand Up @@ -131,4 +131,6 @@ Edge API Routes use the [Edge Runtime](/docs/api-reference/edge-runtime.md), whe

Edge API Routes can [stream responses](/docs/api-reference/edge-runtime.md#web-stream-apis) from the server and run _after_ cached files (e.g. HTML, CSS, JavaScript) have been accessed. Server-side streaming can help improve performance with faster [Time To First Byte (TTFB)](https://web.dev/ttfb/).

> Note: Using [Edge Runtime](/docs/api-reference/edge-runtime.md) with `getServerSideProps` does not give you access to the response object. If you need access to `res`, you should use the [Node.js runtime](/docs/advanced-features/react-18/switchable-runtime.md) by setting `runtime: 'nodejs'`.

View the [supported APIs](/docs/api-reference/edge-runtime.md) and [unsupported APIs](/docs/api-reference/edge-runtime.md#unsupported-apis) for the Edge Runtime.
12 changes: 12 additions & 0 deletions docs/basic-features/data-fetching/get-server-side-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ It can be tempting to reach for an [API Route](/docs/api-routes/introduction.md)

Take the following example. An API route is used to fetch some data from a CMS. That API route is then called directly from `getServerSideProps`. This produces an additional call, reducing performance. Instead, directly import the logic used inside your API Route into `getServerSideProps`. This could mean calling a CMS, database, or other API directly from inside `getServerSideProps`.

### getServerSideProps with Edge API Routes

`getServerSideProps` can be used with both Serverless and Edge Runtimes, and you can set props in both. However, currently in Edge Runtime, you do not have access to the response object. This means that you cannot — for example — add cookies in `getServerSideProps`. To have access to the response object, you should **continue to use the Node.js runtime**, which is the default runtime.

You can explicitly set the runtime on a [per-page basis](https://nextjs.org/docs/advanced-features/react-18/switchable-runtime#page-runtime-option) by modifying the `config`, for example:

```js
export const config = {
runtime: 'nodejs',
}
```

## Fetching data on the client side

If your page contains frequently updating data, and you don’t need to pre-render the data, you can fetch the data on the [client side](/docs/basic-features/data-fetching/client-side.md). An example of this is user-specific data:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ description: 'Learn how to create or update static pages at runtime with Increme

Next.js allows you to create or update static pages _after_ you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, **without needing to rebuild the entire site**. With ISR, you can retain the benefits of static while scaling to millions of pages.

> Note: the `experimental-edge` runtime (https://nextjs.org/docs/api-reference/edge-runtime) is currently not compatible with ISR although can leverage `stale-while-revalidate` by setting the `cache-control` header manually.
> Note: The [`experimental-edge` runtime](https://nextjs.org/docs/api-reference/edge-runtime) is currently not compatible with ISR, although can leverage `stale-while-revalidate` by setting the `cache-control` header manually.

To use ISR, add the `revalidate` prop to `getStaticProps`:

Expand Down
2 changes: 1 addition & 1 deletion docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"path": "/docs/api-routes/response-helpers.md"
},
{
"title": "Edge API Routes (Beta)",
"title": "Edge API Routes",
"path": "/docs/api-routes/edge-api-routes.md"
}
]
Expand Down
2 changes: 1 addition & 1 deletion errors/edge-dynamic-code-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ You can relax the check to allow specific files with your Middleware or Edge API

```typescript
export const config = {
runtime: 'experimental-edge', // for Edge API Routes only
runtime: 'edge', // for Edge API Routes only
unstable_allowDynamic: [
'/lib/utilities.js', // allows a single file
'/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module
Expand Down
2 changes: 1 addition & 1 deletion errors/middleware-upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ If you were previously using Middleware to forward headers to an external API, y
import { type NextRequest } from 'next/server'

export const config = {
runtime: 'experimental-edge',
runtime: 'edge',
}

export default async function handler(req: NextRequest) {
Expand Down
2 changes: 2 additions & 0 deletions errors/returning-response-body-in-middleware.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Returning response body in Middleware

> Note: In Next.js v13.0.0 you can now respond to Middleware directly by returning a `NextResponse`. For more information, see [Producing a Response](https://nextjs.org/docs/advanced-features/middleware#producing-a-response).

#### Why This Error Occurred

[Middleware](https://nextjs.org/docs/advanced-features/middleware) can no longer produce a response body as of `v12.2+`.
Expand Down
2 changes: 1 addition & 1 deletion examples/with-webassembly/pages/api/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export default async function handler() {
return new Response(`got: ${number}`)
}

export const config = { runtime: 'experimental-edge' }
export const config = { runtime: 'edge' }
2 changes: 1 addition & 1 deletion packages/next/build/analysis/extract-const-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function extractValue(node: Node, path?: string[]): any {

/**
* Extracts the value of an exported const variable named `exportedName`
* (e.g. "export const config = { runtime: 'experimental-edge' }") from swc's AST.
* (e.g. "export const config = { runtime: 'edge' }") from swc's AST.
* The value must be one of (or throws UnsupportedValueError):
* - string
* - boolean
Expand Down
52 changes: 34 additions & 18 deletions packages/next/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { isServerRuntime } from '../../server/config-shared'
import type { NextConfig } from '../../server/config-shared'
import type { Middleware, RouteHas } from '../../lib/load-custom-routes'

import { promises as fs } from 'fs'
import { matcher } from 'next/dist/compiled/micromatch'
import { ServerRuntime } from 'next/types'
import {
extractExportedConstValue,
UnsupportedValueError,
} from './extract-const-value'
import { parseModule } from './parse-module'
import { promises as fs } from 'fs'
import { tryToParsePath } from '../../lib/try-to-parse-path'
import * as Log from '../output/log'
import { SERVER_RUNTIME } from '../../lib/constants'
import { ServerRuntime } from 'next/types'
import { checkCustomRoutes } from '../../lib/load-custom-routes'
import { matcher } from 'next/dist/compiled/micromatch'
import { tryToParsePath } from '../../lib/try-to-parse-path'
import { isAPIRoute } from '../../lib/is-api-route'
import { isEdgeRuntime } from '../../lib/is-edge-runtime'
import { RSC_MODULE_TYPES } from '../../shared/lib/constants'

export interface MiddlewareConfig {
Expand Down Expand Up @@ -229,13 +231,13 @@ function getMiddlewareConfig(
return result
}

let warnedAboutExperimentalEdgeApiFunctions = false
function warnAboutExperimentalEdgeApiFunctions() {
if (warnedAboutExperimentalEdgeApiFunctions) {
let warnedAboutExperimentalEdge = false
function warnAboutExperimentalEdge() {
if (warnedAboutExperimentalEdge) {
return
}
Log.warn(`You are using an experimental edge runtime, the API might change.`)
warnedAboutExperimentalEdgeApiFunctions = true
warnedAboutExperimentalEdge = true
}

const warnedUnsupportedValueMap = new Map<string, boolean>()
Expand Down Expand Up @@ -307,7 +309,8 @@ export async function getPageStaticInfo(params: {

if (
typeof resolvedRuntime !== 'undefined' &&
!isServerRuntime(resolvedRuntime)
resolvedRuntime !== SERVER_RUNTIME.nodejs &&
!isEdgeRuntime(resolvedRuntime)
) {
const options = Object.values(SERVER_RUNTIME).join(', ')
if (typeof resolvedRuntime !== 'string') {
Expand All @@ -326,15 +329,28 @@ export async function getPageStaticInfo(params: {

const requiresServerRuntime = ssr || ssg || pageType === 'app'

resolvedRuntime =
SERVER_RUNTIME.edge === resolvedRuntime
? SERVER_RUNTIME.edge
: requiresServerRuntime
? resolvedRuntime || nextConfig.experimental?.runtime
: undefined
resolvedRuntime = isEdgeRuntime(resolvedRuntime)
? resolvedRuntime
: requiresServerRuntime
? resolvedRuntime || nextConfig.experimental?.runtime
: undefined

if (resolvedRuntime === SERVER_RUNTIME.edge) {
warnAboutExperimentalEdgeApiFunctions()
if (resolvedRuntime === SERVER_RUNTIME.experimentalEdge) {
warnAboutExperimentalEdge()
}

if (
resolvedRuntime === SERVER_RUNTIME.edge &&
pageType === 'pages' &&
page &&
!isAPIRoute(page.replace(/^\/pages\//, '/'))
) {
const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.`
Log.error(message)

if (!isDev) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small todo for later.

Suggested change
if (!isDev) {
// TODO: replace process.exit(1) with throwing.
if (!isDev) {

process.exit(1)
}
}

const middlewareConfig = getMiddlewareConfig(
Expand Down
12 changes: 6 additions & 6 deletions packages/next/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import chalk from 'next/dist/compiled/chalk'
import { posix, join } from 'path'
import { stringify } from 'querystring'
import {
API_ROUTE,
PAGES_DIR_ALIAS,
ROOT_DIR_ALIAS,
APP_DIR_ALIAS,
SERVER_RUNTIME,
WEBPACK_LAYERS,
} from '../lib/constants'
import { isAPIRoute } from '../lib/is-api-route'
import { isEdgeRuntime } from '../lib/is-edge-runtime'
import { APP_CLIENT_INTERNALS, RSC_MODULE_TYPES } from '../shared/lib/constants'
import {
CLIENT_STATIC_FILES_RUNTIME_AMP,
Expand Down Expand Up @@ -175,7 +175,7 @@ export function getEdgeServerEntry(opts: {
return `next-middleware-loader?${stringify(loaderParams)}!`
}

if (opts.page.startsWith('/api/') || opts.page === '/api') {
if (isAPIRoute(opts.page)) {
const loaderParams: EdgeFunctionLoaderOptions = {
absolutePagePath: opts.absolutePagePath,
page: opts.page,
Expand Down Expand Up @@ -257,8 +257,8 @@ export async function runDependingOnPageType<T>(params: {
await params.onEdgeServer()
return
}
if (params.page.match(API_ROUTE)) {
if (params.pageRuntime === SERVER_RUNTIME.edge) {
if (isAPIRoute(params.page)) {
if (isEdgeRuntime(params.pageRuntime)) {
await params.onEdgeServer()
return
}
Expand All @@ -279,7 +279,7 @@ export async function runDependingOnPageType<T>(params: {
await Promise.all([params.onClient(), params.onServer()])
return
}
if (params.pageRuntime === SERVER_RUNTIME.edge) {
if (isEdgeRuntime(params.pageRuntime)) {
await Promise.all([params.onClient(), params.onEdgeServer()])
return
}
Expand Down
Loading