Skip to content

baseinc/next-rest-framework

 
 

Repository files navigation


Next REST Framework

Type-safe, self-documenting APIs for Next.js


CI status Github Stars License Contributor Covenant 2.1

Table of contents

Next REST Framework is an open-source, opinionated, lightweight, easy-to-use set of tools to build type-safe, self-documenting APIs with Next.js. Building OpenAPI specification-compliant APIs can be cumbersome and slow but Next REST Framework makes this easy with auto-generated OpenAPI documents and docs using TypeScript and object schemas.

This is a monorepo containing the following packages / projects:

  1. The primary next-rest-framework package
  2. An example application for live demo and local development

Lightweight, type-safe, easy to use

  • Designed to work with TypeScript so that your requests and responses are strongly typed.
  • Supports API endpoints using both RESTful and RPC principles.
  • Object-schema validation with Zod. The object schemas are automatically converted to JSON schema format for the auto-generated OpenAPI specification.
  • Auto-generated and extensible openapi.json spec file from your business logic.
  • Auto-generated Redoc and/or SwaggerUI documentation frontend.
  • Works with Next.js Middleware and other server-side libraries, like NextAuth.js.
  • Supports both Next.js App Router and Pages Router, even at the same time.
  • Fully customizable and compatible with any existing Next.js project.
npm install next-rest-framework

To get access to the auto-generated documentation, initialize the docs endpoint somewhere in your codebase. You can also skip this step if you don't want to expose a public API documentation.

App Router:

// src/app/api/route.ts

import { docsRoute } from 'next-rest-framework';

export const { GET } = docsRoute();

Pages Router:

// src/pages/api.ts

import { docsApiRoute } from 'next-rest-framework';

export default docsApiRoute();

This is enough to get you started. Now you can access the API documentation in your browser. Calling this endpoint will automatically generate the openapi.json OpenAPI specification file, located in the public folder by default. You can also configure this endpoint to disable the automatic generation of the OpenAPI spec file or use the CLI command npx next-rest-framework generate to generate it. You can also create multiple docs endpoints for various use cases. See the full configuration options of this endpoint in the Docs handler options section.

REST

App Router:
// src/app/api/todos/route.ts

import { TypedNextResponse, route, routeOperation } from 'next-rest-framework';
import { z } from 'zod';

const TODOS = [
  {
    id: 1,
    name: 'TODO 1',
    completed: false
  }
];

// Example App Router route handler with GET/POST handlers.
export const { GET, POST } = route({
  getTodos: routeOperation({
    method: 'GET',
    // Optional OpenAPI operation documentation.
    openApiOperation: {
      tags: ['example-api', 'todos', 'app-router']
    }
  })
    // Output schema for strictly-typed responses and OpenAPI documentation.
    .outputs([
      {
        status: 200,
        contentType: 'application/json',
        schema: z.array(
          z.object({
            id: z.number(),
            name: z.string(),
            completed: z.boolean()
          })
        )
      }
    ])
    .handler(() => {
      // Type-checked response.
      return TypedNextResponse.json(TODOS, {
        status: 200
      });
    }),

  createTodo: routeOperation({
    method: 'POST',
    // Optional OpenAPI operation documentation.
    openApiOperation: {
      tags: ['example-api', 'todos', 'app-router']
    }
  })
    // Input schema for strictly-typed request, request validation and OpenAPI documentation.
    .input({
      contentType: 'application/json',
      body: z.object({
        name: z.string()
      })
    })
    // Output schema for strictly-typed responses and OpenAPI documentation.
    .outputs([
      {
        status: 201,
        contentType: 'application/json',
        schema: z.string()
      },
      {
        status: 401,
        contentType: 'application/json',
        schema: z.string()
      }
    ])
    .middleware(
      // Optional middleware logic executed before request validation.
      (req) => {
        if (!req.headers.get('authorization')) {
          // Type-checked response.
          return TypedNextResponse.json('Unauthorized', {
            status: 401
          });
        }
      }
    )
    .handler(async (req) => {
      const { name } = await req.json(); // Strictly-typed request.

      // Type-checked response.
      return TypedNextResponse.json(`New TODO created: ${name}`, {
        status: 201
      });
    })
});

The TypedNextResponse ensures that the response status codes and content-type headers are type-checked. You can still use the regular NextResponse if you prefer to have less type-safety.

Pages Router:
// src/pages/api/todos.ts

import { apiRoute, apiRouteOperation } from 'next-rest-framework';
import { z } from 'zod';

const TODOS = [
  {
    id: 1,
    name: 'TODO 1',
    completed: false
  }
];

// Example Pages Router API route with GET/POST handlers.
export default apiRoute({
  getTodos: apiRouteOperation({
    method: 'GET',
    // Optional OpenAPI operation documentation.
    openApiOperation: {
      tags: ['example-api', 'todos', 'pages-router']
    }
  })
    // Output schema for strictly-typed responses and OpenAPI documentation.
    .outputs([
      {
        status: 200,
        contentType: 'application/json',
        schema: z.array(
          z.object({
            id: z.number(),
            name: z.string(),
            completed: z.boolean()
          })
        )
      }
    ])
    .handler((_req, res) => {
      // Type-checked response.
      res.status(200).json(TODOS);
    }),

  createTodo: apiRouteOperation({
    method: 'POST',
    // Optional OpenAPI operation documentation.
    openApiOperation: {
      tags: ['example-api', 'todos', 'pages-router']
    }
  })
    // Input schema for strictly-typed request, request validation and OpenAPI documentation.
    .input({
      contentType: 'application/json',
      body: z.object({
        name: z.string()
      })
    })
    // Output schema for strictly-typed responses and OpenAPI documentation.
    .outputs([
      {
        status: 201,
        contentType: 'application/json',
        schema: z.string()
      },
      {
        status: 401,
        contentType: 'application/json',
        schema: z.string()
      }
    ])
    // Optional middleware logic executed before request validation.
    .middleware((req, res) => {
      if (!req.headers.authorization) {
        res.status(401).json('Unauthorized'); // Type-checked response.
      }
    })
    .handler((req, res) => {
      const { name } = req.body; // Strictly-typed request.
      res.status(201).json(`New TODO created: ${name}`); // Type-checked response.
    })
});

All of above type-safe endpoints will be now auto-generated to your OpenAPI spec and exposed in the documentation:

Next REST Framework docs

Client

To achieve end-to-end type-safety, you can use any client implementation that relies on the generated OpenAPI specification, e.g. openapi-client-axios.

You can also define your APIs with RPC route handlers that also auto-generate the OpenAPI spec. The RPC endpoints can be consumed with the type-safe API client for end-to-end type safety.

App Router:
// src/app/api/rpc/route.ts

import { rpcOperation, rpcRoute } from 'next-rest-framework';
import { z } from 'zod';

const TODOS = [
  {
    id: 1,
    name: 'TODO 1',
    completed: false
  }
];

const todoSchema = z.object({
  id: z.number(),
  name: z.string(),
  completed: z.boolean()
});

// Example App Router RPC handler.
const { POST, client } = rpcRoute({
  getTodos: rpcOperation()
    // Output schema for strictly-typed responses and OpenAPI documentation.
    .outputs([
      {
        schema: z.array(todoSchema)
      }
    ])
    .handler(() => {
      // Type-checked response.
      return TODOS;
    }),

  getTodoById: rpcOperation()
    .input(z.string())
    .outputs([
      {
        schema: z.object({
          error: z.string()
        })
      },
      {
        schema: todoSchema
      }
    ])
    .handler((id) => {
      const todo = TODOS.find((t) => t.id === Number(id));

      if (!todo) {
        // Type-checked response.
        return { error: 'TODO not found.' };
      }

      // Type-checked response.
      return todo;
    }),

  createTodo: rpcOperation()
    // Input schema for strictly-typed request, request validation and OpenAPI documentation.
    .input(
      z.object({
        name: z.string()
      })
    )
    // Output schema for strictly-typed responses and OpenAPI documentation.
    .outputs([{ schema: todoSchema }])
    .handler(async ({ name: _name }) => {
      // Create todo.
      const todo = { id: 2, name: _name, completed: false };

      // Type-checked response.
      return todo;
    }),

  deleteTodo: rpcOperation()
    .input(z.string())
    .outputs([
      { schema: z.object({ error: z.string() }) },
      { schema: z.object({ message: z.string() }) }
    ])
    .handler((id) => {
      // Delete todo.
      const todo = TODOS.find((t) => t.id === Number(id));

      if (!todo) {
        // Type-checked response.
        return {
          error: 'TODO not found.'
        };
      }

      // Type-checked response.
      return { message: 'TODO deleted.' };
    })
});

export { POST };

export type AppRouterRpcClient = typeof client;
Pages Router:
// src/pages/api/rpc.ts

import { rpcApiRoute } from 'next-rest-framework';

// Example Pages Router RPC handler.
const handler = rpcApiRoute({
  // ...
  // Exactly the same as the App Router example.
});

export default handler;

export type RpcClient = typeof handler.client;

The RPC routes will also be included in your OpenAPI spec now. Note that the rpcOperation definitions can be also be placed outside the rpcRouteHandler if you do not want to expose them as public APIs as long as they're called server-side.

Client

The strongly-typed RPC operations can be called inside inside React server components and server actions like any functions:

'use server';

import { client } from 'app/api/rpc/route';

export default async function Page() {
  const todos = await client.getTodos();

  const createTodo = async (name: string) => {
    'use server';
    return client.createTodo({ name });
  };

  // ...
}

For client-rendered components you can use the strongly-typed rpcClient or use server actions from the above example:

'use client';

import { useState } from 'react';
import { rpcClient } from 'next-rest-framework/rpc-client';
import { type RpcClient } from 'app/api/rpc/route';

const client = rpcClient<RpcClient>({
  url: 'http://localhost:3000/api/rpc'
});

export default function Page() {
  // ...

  useEffect(() => {
    client
      .getTodos()
      .then(() => {
        // ...
      })
      .catch(console.error);
  }, []);

  const createTodo = async (name: string) => {
    'use server';
    return client.createTodo({ name });
  };

  // ...
}

The rpcClient calls can also be easily integrated with any data fetching framework, like React Query or RTKQ.

The following options can be passed to the docsRouteHandler (App Router) and docsApiRouteHandler (Pages Router) functions for customizing Next REST Framework:

Name Description
deniedPaths Array of paths that are denied by Next REST Framework and not included in the OpenAPI spec. Supports wildcards using asterisk * and double asterisk ** for recursive matching. Example: ['/api/disallowed-path', '/api/disallowed-path-2/*', '/api/disallowed-path-3/**'] Defaults to no paths being disallowed.
allowedPaths Array of paths that are allowed by Next REST Framework and included in the OpenAPI spec. Supports wildcards using asterisk * and double asterisk ** for recursive matching. Example: ['/api/allowed-path', '/api/allowed-path-2/*', '/api/allowed-path-3/**'] Defaults to all paths being allowed.
openApiObject An OpenAPI Object that can be used to override and extend the auto-generated specification.
openApiJsonPath Path that will be used for fetching the OpenAPI spec - defaults to /openapi.json. This path also determines the path where this file will be generated inside the public folder.
autoGenerateOpenApiSpec Setting this to false will not automatically update the generated OpenAPI spec when calling the docs handler endpoints. Defaults to true.
docsConfig A Docs config object for customizing the generated docs.
suppressInfo Setting this to true will suppress all informational logs from Next REST Framework. Defaults to false.

The docs config options can be used to customize the generated docs:

Name Description
provider Determines whether to render the docs using Redoc (redoc) or SwaggerUI swagger-ui. Defaults to redoc.
title Custom title, used for the visible title and HTML title.
description Custom description, used for the visible description and HTML meta description.
faviconUrl Custom HTML meta favicon URL.
logoUrl A URL for a custom logo.

REST

The following options cam be passed to the routeHandler (App Router) and apiRouteHandler (Pages Router) functions to create new API endpoints:

Name Description Required
GET | PUT | POST | DELETE | OPTIONS | HEAD | PATCH A Method handler object. true
openApiPath An OpenAPI Path Item Object that can be used to override and extend the auto-generated specification. false

The route operation functions routeOperation (App Router) and apiRouteOperation (Pages Router) allow you to define your API handlers for your endpoints. These functions accept an OpenAPI Operation object as a parameter, that can be used to override the auto-generated specification. Calling this function allows you to chain your API handler logic with the following functions.

Name Description
input A Route operation input function for defining the validation and documentation of the request.
outputs An Route operation outputs function for defining the validation and documentation of the response.
handler A Route operation-handler function for defining your business logic.
middleware A Route operation middleware function that gets executed before the request input is validated.

The route operation input function is used for type-checking, validation and documentation of the request, taking in an object with the following properties:

Name Description Required
contentType The content type header of the request. When the content type is defined, a request with an incorrect content type header will get an error response. false
body A Zod schema describing the format of the request body. When the body schema is defined, a request with an invalid request body will get an error response. false
query A Zod schema describing the format of the query parameters. When the query schema is defined, a request with invalid query parameters will get an error response. false

Calling the route operation input function allows you to chain your API handler logic with the Route operation outputs, Route operation middleware and Route operation handler functions.

The route operation outputs function is used for type-checking and documentation of the response, taking in an array of objects with the following properties:

Name Description Required
status A status code that your API can return. true
contentType The content type header of the response. true
schema A Zod schema describing the format of the response data.  true

Calling the route operation outputs function allows you to chain your API handler logic with the Route operation middleware and Route operation handler functions.

The route operation middleware function is executed before validating the request input. The function takes in the same parameters as the Next.js Route Handlers and API Routes handlers.

Calling the route operation middleware function allows you to chain your API handler logic with the Handler function.

The route operation handler function is a strongly-typed function to implement the business logic for your API. The function takes in strongly-typed versions of the same parameters as the Next.js Route Handlers and API Routes handlers.

RPC

The rpcRouteHandler (App Router) and rpcApiRouteHandler (Pages Router) functions allow the following options as the second parameter after passing your RPC operations.

Name Description Required
openApiPath An OpenAPI Path Item Object that can be used to override and extend the auto-generated specification. false
openApiOperation An OpenAPI Path Item Object that can be used to override and extend the auto-generated specification. false

The rpcOperation function allows you to define your API handlers for your RPC endpoint. Calling this function allows you to chain your API handler logic with the following functions.

Name Description
input An RPC operation input function for defining the validation and documentation of the operation.
outputs An RPC operation outputs function for defining the validation and documentation of the response.
handler An RPC operation handler function for defining your business logic.
middleware An RPC operation middleware function that gets executed before the operation input is validated.

The RPC operation input function is used for type-checking, validation and documentation of the RPC call. It takes in a A Zod schema as a parameter that describes the format of the operation input. When the input schema is defined, an RPC call with invalid input will get an error response.

Calling the RPC input function allows you to chain your API handler logic with the RPC operation outputs, RPC middleware and RPC handler functions.

The RPC operation outputs function is used for type-checking and documentation of the response, taking in an array of objects with the following properties:

Name Description Required
schema A Zod schema describing the format of the response data.  true
name An optional name used in the generated OpenAPI spec, e.g. GetTodosErrorResponse. false

Calling the RPC operation outputs function allows you to chain your API handler logic with the RPC operation middleware and RPC operation handler functions.

The RPC operation middleware function is executed before validating RPC operation input. The function takes in strongly typed parameters typed by the RPC operation input function.

Calling the RPC operation middleware function allows you to chain your RPC API handler logic with the RPC operation handler function.

The RPC operation handler function is a strongly-typed function to implement the business logic for your API. The function takes in strongly typed parameters typed by the RPC operation input function.

The Next REST Framework CLI supports generating and validating the openapi.json file:

  • npx next-rest-framework generate to generate the openapi.json file.
  • npx next-rest-framework validate to validate that the openapi.json file is up-to-date.

The next-rest-framework validate command is useful to have as part of the static checks in your CI/CD pipeline. Both commands support the following options:

Name Description
--skipBuild <boolean> By default, next build is used to build your routes. If you have already created the build, you can skip this step by setting this to true.
--distDir <string> Path to your production build directory. Defaults to .next.
--timeout <string> The timeout for generating the OpenAPI spec. Defaults to 60 seconds.
--configPath <string> In case you have multiple docs handlers with different configurations, you can specify which configuration you want to use by providing the path to the API. Example: /api/my-configuration.
--debug <boolean> Inherit and display logs from the next build command. Defaults to false.

See the changelog in CHANGELOG.md

All contributions are welcome!

ISC, see full license in LICENSE.

About

Type-safe, self-documenting REST APIs for Next.js

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 91.6%
  • JavaScript 8.4%