Coder Social home page Coder Social logo

nextlove's Introduction

nextlove - Enhanced NextJS API Types, OpenAPI and Utilities

Make type-safe routes that automatically generate OpenAPI in NextJS easy!

  • Define endpoints with middleware and have your request objects and responses automatically be typed
  • The same zod schemas used for your types will be in the generated openapi.json file!
  • Throw http exceptions and they'll magically be handled
  • Have well-typed middleware

Installation

yarn add nextlove

Create well-typed routes + middleware with nextlove!

nextlove allows you to create well-typed middleware and routes using utility types and functions. The two main functions to know are createWithRouteSpec, which allows you to create a withRouteSpec function that can be used with all your endpoints, and the Middleware utility function which makes middleware type-safe.

Let's take a look at an example project with three files:

  • lib/with-route-spec.ts - This file is used to create the withRouteSpec middleware. This middleware should be used for all your routes.
  • lib/middlewares/with-auth-token.ts - This is an authentication middleware we'll be using to make sure requests are authenticating
  • lib/middlewares/with-db.ts - A common global middleware that attaches a database client to the request object
  • pages/api/health.ts - Just a health endpoint to see if the server is running! It won't have any auth
  • pages/api/todos/add.ts - An endpoint to add a TODO, this will help show how we can use auth!
// pages/api/health.ts
import { withRouteSpec } from "lib/with-route-spec"
import { z } from "zod"

const routeSpec = {
  methods: ["GET"],
  auth: "none",
  jsonResponse: z.object({
    healthy: z.boolean(),
  }),
} as const

export default withRouteSpec(routeSpec)(async (req, res) => {
  /* ... */
  return res.status(200).json({ healthy: true })
})
// lib/with-route-spec.ts
export const withRouteSpec = createWithRouteSpec({
  authMiddlewareMap: { auth_token: withAuthToken },
  globalMiddlewares: [globalMiddleware],

  // For OpenAPI Generation
  apiName: "My API",
  productionServerUrl: "https://example.com",
  globalSchemas: {
    user: z.object({
      user_id: z.string().uuid(),
    }),
  },
} as const)
// lib/middlewares/with-auth-token.ts
import { UnauthorizedException, Middleware } from "nextlove"

export const withAuthToken: Middleware<{
  auth: {
    authorized_by: "auth_token"
  }
}> = (next) => async (req, res) => {
  req.auth = {
    authorized_by: "auth_token",
  }

  return next(req, res)
}

export default withAuthToken
// pages/api/todos/add.ts
import { withRouteSpec, UnauthorizedException } from "lib/with-route-spec"
import { z } from "zod"

const routeSpec = {
  methods: ["POST"],
  auth: "auth_token",
  jsonBody: z.object({
    content: z.string(),
  }),
  jsonResponse: z.object({
    ok: z.boolean(),
  }),
} as const

export default withRouteSpec(routeSpec)(async (req, res) => {
  // req.auth is correctly typed here!
  if (req.auth.authorized_by !== "auth_token") {
    throw new UnauthorizedException({
      type: "unauthorized",
      message: "Authenticate yourself to get the requested response",
    })
  }
  // TODO add todo
  return res.status(200).json({ ok: true })
})

createWithRouteSpec Parameters

Parameter Description
authMiddlewareMap Object that maps different types of auth to their middleware
globalMiddlewares Middlewares that should be applied on every route
apiName Used as the name of the api in openapi.json
productionServerUrl Used as the default server url in openapi.json

withRouteSpec Parameters

Parameter Description
methods HTTP Methods accepted by this route
auth none or a key from your authMiddlewareMap, this authentication middleware will be applied
queryParams Any GET query parameters on the request as a zod object
jsonBody The JSON body this endpoint accepts as a zod object
formData The multipart/form-data (todo) or application/x-www-form-urlencoded encoded body
commonParams Parameters common to both the query and json body as a zod object, this is sometimes used if a GET route also accepts POST
jsonResponse A zod object representing the json resposne

Generating OpenAPI Types (Command Line)

Just run nextlove generate-openapi in your project root!

Examples:

# Print OpenAPI JSON directly to the command line for the package in the current directory
nextlove generate-openapi --packageDir .

# Write OpenAPI JSON to "openapi.json" file
nextlove generate-openapi . --outputFile openapi.json

# Only generate OpenAPI JSON for public api routes
nextlove generate-openapi . --pathGlob '/pages/api/public/**/*.ts'
Parameter Description
packageDir Path to directory containing package.json and NextJS project
outputFile Path to output openapi.json file
pathGlob Paths to consider as valid routes for OpenAPI generation, defaults to /pages/api/**/*.ts

Generating OpenAPI Types (Script)

import { generateOpenAPI } from "nextlove"

generateOpenAPI({
  packageDir: ".",
  outputFile: "openapi.json",
  pathGlob: "/src/pages/api/**/*.ts",

  // Tags improve the organization of an OpenAPI spec by making "expandable"
  // sections including routes
  tags: ["users", "teams", "workspaces"].map((t) => ({
    name: `/${t}`,
    description: t,
    doesRouteHaveTag: (route) => route.includes(`/${t}`),
  })),
  mapFilePathToHTTPRoute(fp) {
    return fp
      .replace("src/pages/api/public", "")
      .replace(/\.ts$/, "")
      .replace(/\/index$/, "")
  },
})

Extracting route specs (Command Line)

Just run nextlove extract-route-specs in your project root! It will output a ESM file bundled by esbuild.

Caveats:

  • All dependencies and dev dependencies in your package.json are automatically marked as external when bundling. This means that you may want to re-bundle the output file if you plan on publishing it as part of a library.
  • By default, API route files aren't allowed to import anything besides dependencies declared in package.json. This is to avoid accidentally polluting the bundle. To allow specific imports, use the --allowed-import-patterns flag: --allowed-import-patterns '**/lib/**' --allowed-import-patterns '**/models/**'

Wrap middlewares together using wrappers!

import { wrappers } from "nextlove"

wrappers(withDatabase, logger.withContext("somecontext"), async (req, res) => {
  res.status(200).end("...")
})

nextjs-exception-middleware

import { BadRequestException } from "nextlove"

// Inside a route handler
if (bad_soups.includes(soup_param)) {
  throw new BadRequestException({
    type: "cant_make_soup",
    message: "Soup was too difficult, please specify a different soup",
    data: { soup_param },
  })
}

All Modules

This repo bundles NextJS utility modules including...

nextlove's People

Contributors

abimaelmartell avatar andrii-balitskyi avatar canadaduane avatar codetheweb avatar itelo avatar kainpets avatar mikelittman avatar mxsdev avatar phpnode avatar razor-x avatar rchodava avatar semantic-release-bot avatar seveibar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

nextlove's Issues

Design Type Validation that supports new Vercel App Routes

New Vercel App endpoints don't support res

Could this also help with method-discriminated validation more generally?

export const GET = withRouteSpec.get({
	queryParams: z.object({ })
} as const, async (req) => {
  return Response(200, JSON.stringify("{}"))
//  return NextApiResponse.json({ })
})

Postman Collection Generation

Could be converted from OpenAPI generation

The OpenAPI conversion has a little jank in my experience (auth gets set to seam-workspace as a header for all endpoints). Maybe there's something we can do to give Postman the right "hint" here?

Nextlove Edge Support MVP Steps

In relation to: #82

The PR is too big and dangerous as is. This issue is to design an approach for getting edge-support merged

First let's get some opinions out of the way

  • We will introduce withRouteSpecEdge to avoid a breaking change. This means it's an EDGE-COMPATIBLE withRouteSpec, it still shares the exact same API and types as withRouteSpec
  • We will introduce an example app that uses edge e.g. example-edge-todo-app, this app also ideally deploys somewhere on cloudflare or vercel edge
  • Introduce the edge term sparingly. Nextlove will work on both edge and lambda, so avoid any "edge-specific" terminology

If you disagree with the above, we can't move on to what's below

  1. Propose changes to nextlove API e.g. the req.Response object that make nextlove compatible with edge. Do not introduce withRouteSpecEdge or any special edge handling
  2. After the API changes are in, build withRouteSpecEdge (NOTE: withRouteSpecEdge is lambda and edge compatible)
  3. Replace withRouteSpec with withRouteSpecEdge and deprecate withRouteSpecEdge

CC @itelo @codetheweb @razor-x

Generated route types not compatible with @typescript-eslint/ban-types

      5:18  error  Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead  @typescript-eslint/ban-types

Needs auto publishing

publishing should happen inside of the packages/nextlove directory i.e. the current manual process is cd packages/nextlove && yarn build && npm publish

Note that seam-plop provides the boilerplate github ci release template

Regression: cannot build with tsup starting with version 2.1.0

See error (collapsed) below. Confirmed in this PR seamapi/fake-seam-connect#32

Details
> tsup

CLI Building entry: src/index.ts
CLI Using tsconfig: tsconfig.build.json
CLI tsup v7.1.0
CLI Using tsup config: /home/runner/work/fake-template/fake-template/tsup.config.ts
CLI Target: es2021
CLI Cleaning output folder
ESM Build start
CJS Build start
DTS Build start
DTS ⚡️ Build success in 4582ms
DTS dist/index.d.ts  3.64 KB
DTS dist/index.d.cts 3.64 KB

<--- Last few GCs --->

[2169:0x58d7d00]    40727 ms: Mark-sweep (reduce) 2028.5 (2083.1) -> 2026.4 (2081.5) MB, 2440.7 / 0.0 ms  (+ 2.6 ms in 3 steps since start of marking, biggest step 2.4 ms, walltime since start of marking 2451 ms) (average mu = 0.293, current mu = 0.167) a[2169:0x58d7d00]    43245 ms: Mark-sweep (reduce) 2026.8 (2081.7) -> 2026.8 (2082.0) MB, 2514.4 / 0.0 ms  (average mu = 0.153, current mu = 0.001) allocation failure; GC in old space requested


<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
 1: 0xb7a940 node::Abort() [node]
 2: 0xa8e823  [node]
 3: 0xd5c990 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
 4: 0xd5cd37 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
 5: 0xf3a435  [node]
 6: 0xf4c91d v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
 7: 0xf2701e v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
 8: 0xf283e7 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
 9: 0xf095ba v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [node]
10: 0x12ce9bd v8::internal::Runtime_AllocateInOldGeneration(int, unsigned long*, v8::internal::Isolate*) [node]
11: 0x16fb6f9  [node]
Aborted (core dumped)
Error: Process completed with exit code 134.

Installing `nsm` binary assumes packages installed in sub-directory

The package.json file includes an nsm binary which is from this package's 'nextjs-server-modules' dependency:

  "bin": {
    "nsm": "node_modules/nextjs-server-modules/bin.js",
    "nextlove": "bin.js"
  }

https://github.com/seamapi/nextlove/blob/main/packages/nextlove/package.json#L14

This pattern is not optimal, as it assumes packages are always placed in sub-directories; however, optimizing package managers like pnpm only support this with a flag (and it makes pnpm slower).

When used in its default way, pnpm reports the following error:

 WARN  Failed to create bin at /home/duane/Seam/seamos-backend/node_modules/.bin/nsm. The source file at /home/duane/Seam/seamos-backend/node_modules/nextlove/node_modules/nextjs-server-modules/bin.js does not exist.

Parse .describe as markdown with yaml front matter

Parse Zod description as Markdown with YAML front matter: https://jekyllrb.com/docs/front-matter/
This is a well established system for including metadata inside a plain markdown string.

The front matter would be included in the openapi spec as metadata while the body would be used as the description. Some keys can trigger special behavior, for example injecting @deprecated into the generated types.

For example:

const routeSpec = {
  methods: ["POST"],
  auth: "auth_token",
  jsonBody: z.object({
    name: z.string().describe(`
    ---
    deprecated: Use full_name
    ---
    The name of the user.
    `),
    full_name: z.string().describe("The name of the user."),
  }),
  jsonResponse: z.object({
    ok: z.boolean(),
  }),
} as const

Yarn build fails on Windows

I wrote a more generic title because there are 2 main reasons for that:

First reason is that the build command on package.json is using cp:

"build": "rimraf dist && nsm build && tsup ./index.ts --dts --sourcemap && cp -r .next dist/.next",

Which is not a command on windows. There are many ways to solve this, but if a solution without any extra packages is required, then there probably should be a build-windows command, like:

"build-windows": "rimraf dist && nsm build && tsup ./index.ts --dts --sourcemap && xcopy .next dist\\.next /e /i",

But that would affect other commands. So maybe there could be a discussion on what would be the best solution here.

The second bug occurs in parse-routes-in-package.ts:

The line

const fullPathGlob = path.join(packageDir, pathGlob)

returns an invalid path, at least on windows. It causes

const filepaths = await globby(`${fullPathGlob}`)

to return 0 filepaths.

Globby docs recommend using path.posix.join instead:

image

So it'd be:

// Load all route specs
  const fullPathGlob = path.posix.join(packageDir, pathGlob)
  console.log(`searching "${fullPathGlob}"...`)
  const filepaths = await globby(`${fullPathGlob}`)
  console.log(`found ${filepaths.length} files`)

And that worked for me.

I can attach a PR to this issue if necessary.

Add command to generate Zod schemas for routes

E.x. nextlove generate-route-schemas should output

import {z} from "zod"

const routes = {
  "/hello/world": {
    queryParams: z.object({device_id: z.string()})
    jsonResponse: z.object({message: z.string()})
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.