├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── README.md ├── example ├── .gitignore ├── README.md ├── next-env.d.ts ├── package.json ├── pages │ ├── api │ │ ├── get.ts │ │ ├── nested │ │ │ └── get.ts │ │ └── post.ts │ └── index.tsx ├── tsconfig.json └── yarn.lock ├── index.d.ts ├── index.js ├── jest.config.js ├── netlify.toml ├── package.json ├── src ├── adaptor.ts ├── command.ts └── index.ts ├── templates └── function.js ├── test └── adaptor.test.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | example/ 3 | lib/ 4 | templates/ 5 | index.* 6 | jest.config.js 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "env": { 9 | "node": true, 10 | "browser": true 11 | }, 12 | "rules": { 13 | "@typescript-eslint/explicit-module-boundary-types": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | Thumbs.db 4 | .idea/ 5 | .vscode/ 6 | *.sublime-project 7 | *.sublime-workspace 8 | *.log 9 | coverage/ 10 | lib 11 | .eslintcache 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | .idea/ 4 | .vscode/ 5 | *.sublime-project 6 | *.sublime-workspace 7 | *.log 8 | .eslintcache 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | after_script: 5 | - 'yarn codecov' 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next Export API 2 | 3 | [![Build status](https://travis-ci.org/lucasconstantino/next-export-api.svg?branch=master)](https://travis-ci.org/github/lucasconstantino/next-export-api) 4 | [![codecov](https://codecov.io/gh/lucasconstantino/next-export-api/branch/master/graph/badge.svg)](https://codecov.io/gh/lucasconstantino/next-export-api) 5 | 6 |
7 | 8 | **Use Next.js API Routes on statically exported websites deployed over Netlify** 9 | 10 | _Check out the [working example](https://next-export-api.netlify.app/)_ 11 | 12 | 13 |
14 | 15 | ## Installation 16 | 17 | ``` 18 | yarn add next-export-api 19 | ``` 20 | 21 | or 22 | 23 | ``` 24 | npm install next-export-api 25 | ``` 26 | 27 | > Ensure to install it as a production dependency, not a development one. 28 | 29 | Add `next-export-api` to the build script. You can use `postbuild` if you are using `build` on the `package.json`: 30 | 31 | ```diff 32 | { 33 | "name": "example", 34 | "scripts": { 35 | "build": "next build && next export", 36 | + "postbuild": "next-export-api", 37 | "dev": "next dev", 38 | "start": "next start" 39 | }, 40 | "dependencies": { 41 | "next": "9.5.5", 42 | "react": "16.14.0", 43 | "react-dom": "16.14.0" 44 | } 45 | } 46 | ``` 47 | 48 | Configure `netlify.toml` to define functions directory: 49 | 50 | ```toml 51 | [build] 52 | command = "yarn build" 53 | publish = "out" 54 | functions = "out/api" 55 | ``` 56 | 57 | > Both `publish` and `functions` above are configurable, and will be respected by `next-export-api` command. 58 | 59 | _Please check the [example folder](./example/)_ for a ready to deploy sample project. 60 | 61 | ## Motivation 62 | 63 | [Over a year ago](https://nextjs.org/blog/next-9#api-routes) Next.js released version 9 with [API Routes](https://nextjs.org/docs/api-routes/introduction) support. Netlify has also long supported their vision of serverless [functions](https://www.netlify.com/products/functions/). The issue? They have very diverging APIs. Next.js even states, in a [caveats section](https://nextjs.org/docs/api-routes/introduction#caveats), it's API Routes do not work with `next export` projects - the most straightforward way of deploying Next.js projects on Netlify. However, with a bit of adaptation from generated API Routes entrypoints files, it's possible to make them capable of running on Netlify functions environment. 64 | 65 | ## Similar projects 66 | 67 | This project is heavily inspired by... 68 | 69 | - [`next-on-netlify`](https://github.com/netlify/next-on-netlify) 70 | - [`next-aws-lambda`](https://github.com/serverless-nextjs/serverless-next.js) 71 | - [`serverless-http`](https://github.com/dougmoscrop/serverless-http) 72 | 73 | The difference to these projects is `next-export-api` focuses in allowing the use of API Routs on a Next.js [Static HTML Exported](https://nextjs.org/docs/advanced-features/static-html-export) project. 74 | 75 | ## How does it work 76 | 77 | Similarly to `next-on-netlify`, this project takes advantage of Next.js own building system, and operates on it's generated code. As for API Routes, Next.js does generate a self-contained file for each `/pages/api` entrypoint, meaning we can _import_ those files or of the foreseen context of a Next.js server. 78 | 79 | The process is like the following: 80 | 81 | 1. Next.js builds into the `.next` directory; 82 | 2. Next.js exports static site into (configurable) `out` directory; 83 | 3. `next-export-api` reads page manifests from the `.next` build, and... 84 | 85 | 1. Creates one file for each API route under `out/api` 86 | 87 | These files are thin wrappers over Next.js original ones, only adapting for execution on AWS using well supported [`serverless-http`](https://github.com/dougmoscrop/serverless-http). 88 | 89 | 2. Creates `_redirects` rules for each of the API routes above, mapping `/api/*` calls to `/.netlify/functions/*` as expected by Netlify 90 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /example/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build && next export", 7 | "postbuild": "next-export-api", 8 | "dev": "next dev", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "next": "9.5.5", 13 | "next-export-api": "^0.1.11", 14 | "react": "16.14.0", 15 | "react-dom": "16.14.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.11.8", 19 | "@types/react": "^16.9.52", 20 | "typescript": "^4.0.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/pages/api/get.ts: -------------------------------------------------------------------------------- 1 | const handler = (req, res) => { 2 | res.status(200).send({ name: `Hello, ${req.query.name}` }) 3 | } 4 | 5 | export default handler 6 | -------------------------------------------------------------------------------- /example/pages/api/nested/get.ts: -------------------------------------------------------------------------------- 1 | const handler = (req, res) => { 2 | res.status(200).send({ name: `Hello, nested ${req.query.name}` }) 3 | } 4 | 5 | export default handler 6 | -------------------------------------------------------------------------------- /example/pages/api/post.ts: -------------------------------------------------------------------------------- 1 | const handler = (req, res) => { 2 | res.send({ name: `Hello, ${req.body.name}` }) 3 | } 4 | 5 | export default handler 6 | -------------------------------------------------------------------------------- /example/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react' 2 | 3 | /** 4 | * Generic requester factory. 5 | */ 6 | const useRequester = (requester: (name: string) => Promise) => { 7 | const [loading, setLoading] = useState(false) 8 | const [response, setResponse] = useState('') 9 | 10 | const submit = useCallback(async (name: string) => { 11 | setLoading(true) 12 | 13 | await requester(name) 14 | .then((res) => { 15 | if (res.status !== 200) { 16 | throw new Error(`Request failed with ${res.status}`) 17 | } 18 | 19 | return res.text() 20 | }) 21 | .then(setResponse) 22 | .catch((err) => { 23 | console.error(err) 24 | setResponse('Failed!') 25 | }) 26 | 27 | setLoading(false) 28 | }, []) 29 | 30 | return { response, loading, submit } 31 | } 32 | 33 | const requests = { 34 | get: (name: string) => fetch(`/api/get?name=${name}`), 35 | post: (name: string) => 36 | fetch(`/api/post`, { 37 | method: 'POST', 38 | headers: { 'Content-Type': 'application/json' }, 39 | body: JSON.stringify({ name }), 40 | }), 41 | nested: (name: string) => fetch(`/api/nested/get?name=${name}`), 42 | } 43 | 44 | const Home = () => { 45 | const [name, setName] = useState('') 46 | 47 | const apis = { 48 | get: useRequester(requests.get), 49 | post: useRequester(requests.post), 50 | nested: useRequester(requests.nested), 51 | } 52 | 53 | return ( 54 |
55 | 111 | 112 |
113 |

114 | 118 | next-export-api 119 | {' '} 120 | example 121 |

122 |
123 | 124 |
125 | 126 | 127 | 128 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 150 | 153 | 154 | 155 | 163 | 166 | 167 | 168 | 176 | 179 | 180 | 181 |
129 | setName(e.target.value)} 134 | /> 135 | Response
143 | 149 | 151 | {apis.get.response} 152 |
156 | 162 | 164 | {apis.post.response} 165 |
169 | 175 | 177 | {apis.nested.response} 178 |
182 |
183 |
184 | ) 185 | } 186 | 187 | export default Home 188 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './lib' 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index') 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | } 6 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "example" 3 | command = "yarn build" 4 | publish = "out" 5 | functions = "out/api" 6 | 7 | [dev] 8 | framework = "#static" 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-export-api", 3 | "version": "0.1.11", 4 | "private": false, 5 | "description": "Next.js API Routes adaptor for a Netlify static website", 6 | "repository": "git@github.com:lucasconstantino/next-export-api.git", 7 | "license": "MIT", 8 | "author": "Lucas Constantino Silva ", 9 | "keywords": [ 10 | "nextjs", 11 | "netlify", 12 | "react", 13 | "functions", 14 | "lambda", 15 | "api", 16 | "static", 17 | "jamstack" 18 | ], 19 | "bin": "./lib/command.js", 20 | "main": "./index.js", 21 | "scripts": { 22 | "compile": "tsc", 23 | "lint": "eslint . --cache --fix --ext .ts,.tsx", 24 | "test": "jest", 25 | "type-check": "tsc --noEmit", 26 | "qa": "yarn lint && yarn test && yarn tsc --noEmit", 27 | "prepublish": "yarn qa && yarn compile" 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "lint-staged", 32 | "pre-push": "yarn qa" 33 | } 34 | }, 35 | "lint-staged": { 36 | "*.{js,ts,tsx}": [ 37 | "eslint --fix", 38 | "prettier --write" 39 | ], 40 | "*.{json,md}": [ 41 | "prettier --write" 42 | ] 43 | }, 44 | "dependencies": { 45 | "body-parser": "^1.19.0", 46 | "express": "^4.17.1", 47 | "find-up": "^5.0.0", 48 | "serverless-http": "^2.6.0", 49 | "signale": "^1.4.0", 50 | "toml": "^3.0.0" 51 | }, 52 | "devDependencies": { 53 | "@types/aws-lambda": "^8.10.64", 54 | "@types/aws-serverless-express": "^3.3.3", 55 | "@types/jest": "^26.0.14", 56 | "@types/next": "^9.0.0", 57 | "@types/node": "^14.11.8", 58 | "@types/react": "^16.9.52", 59 | "@types/signale": "^1.4.1", 60 | "@types/supertest": "^2.0.10", 61 | "@types/webpack": "^4.41.22", 62 | "@typescript-eslint/eslint-plugin": "^4.4.1", 63 | "@typescript-eslint/parser": "^4.4.1", 64 | "codecov": "^3.8.0", 65 | "eslint": "^7.11.0", 66 | "husky": ">=4", 67 | "jest": "^26.5.3", 68 | "lint-staged": ">=10", 69 | "prettier": ">=2", 70 | "supertest": "^5.0.0", 71 | "ts-jest": "^26.4.1", 72 | "typescript": "^4.0.3" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/adaptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This can be improved to mimic better Next.js API 3 | * @see https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/api-utils.ts 4 | */ 5 | 6 | import type { NextApiHandler } from 'next' 7 | import type { Context, APIGatewayEvent } from 'aws-lambda' 8 | 9 | import express, { Handler } from 'express' 10 | import { json } from 'body-parser' 11 | import serverless from 'serverless-http' 12 | 13 | const adaptor = (handler: NextApiHandler) => ( 14 | event: APIGatewayEvent, 15 | context: Context 16 | ) => { 17 | const app = express() 18 | .use(json()) // compatibility with Next.js default parsers 19 | .use((handler as unknown) as Handler) 20 | 21 | /** 22 | * Execute the event/context using the Next.js adapted handler. 23 | */ 24 | return serverless(app)(event, context) 25 | } 26 | 27 | export { adaptor } 28 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path' 4 | import { promises as fs } from 'fs' 5 | import findUp from 'find-up' 6 | import { parse } from 'toml' 7 | import { Signale } from 'signale' 8 | 9 | type Config = { publish?: string; functions?: string } 10 | type NetlifyToml = { build?: Config } 11 | type PromisedResult = T extends PromiseLike ? U : T 12 | 13 | const noLabel = { label: '' } 14 | const options = { types: { info: noLabel, success: noLabel } } as any 15 | const log = new Signale(options).scope('Next Export API') 16 | 17 | const defaults = { functions: 'out/api' } 18 | 19 | /** 20 | * Simple faulty version of an async path exists check. 21 | */ 22 | const exists = (filepath: string) => 23 | fs 24 | .stat(filepath) 25 | .then(() => true) 26 | .catch(() => false) 27 | 28 | /** 29 | * Load Netlify config from netlify.toml file. 30 | */ 31 | const getNetlifyConfig = async (): Promise => { 32 | const toml = await findUp('netlify.toml') 33 | return toml ? parse(await fs.readFile(toml, 'utf-8')) : { build: defaults } 34 | } 35 | 36 | /** 37 | * Find out _redirects file location. 38 | */ 39 | const getRedirectsPath = async (cwd: string, { build }: NetlifyToml) => 40 | path.resolve(cwd, build?.publish ?? 'out', '_redirects') 41 | 42 | /** 43 | * Find output functions dir based on netlify.toml or a default. 44 | */ 45 | const getFunctionOutPath = async (cwd: string, { build }: NetlifyToml) => { 46 | // find the output directory for the functions. 47 | const functions = 48 | build?.functions ?? build?.publish 49 | ? `${build?.publish}/api` 50 | : defaults.functions 51 | 52 | return path.resolve(cwd, functions) 53 | } 54 | 55 | /** 56 | * Resolve all relevant paths. 57 | */ 58 | const getPaths = async () => { 59 | const cwd = process.cwd() 60 | 61 | // templates 62 | const templates = path.resolve(__dirname, '../templates') 63 | 64 | // build 65 | const server = path.resolve(cwd, '.next/server/') 66 | const pagesManifest = path.resolve(server, 'pages-manifest.json') 67 | 68 | const toml = await getNetlifyConfig() 69 | 70 | // output 71 | const redirects = await getRedirectsPath(cwd, toml) 72 | const functions = await getFunctionOutPath(cwd, toml) 73 | 74 | return { cwd, server, pagesManifest, redirects, functions, templates } 75 | } 76 | 77 | type Paths = PromisedResult> 78 | 79 | /** 80 | * Get all API routes as built by Next.js 81 | */ 82 | const getAPIRoutes = async (paths: Paths) => { 83 | const routes: [string, string][] = [] 84 | const manifest = await import(paths.pagesManifest) 85 | 86 | for (const [endpoint, filepath] of Object.entries(manifest.default)) { 87 | if (endpoint.startsWith('/api/')) { 88 | routes.push([ 89 | endpoint, 90 | // Function file paths are used relative to the output function directory. 91 | path.relative( 92 | paths.functions, 93 | path.resolve(paths.server, filepath as string) 94 | ), 95 | ]) 96 | } 97 | } 98 | 99 | return routes 100 | } 101 | 102 | const run = async () => { 103 | log.info('Initiating Next Export API') 104 | 105 | const paths = await getPaths() 106 | log.info('Resolved paths') 107 | 108 | const template = await fs.readFile( 109 | path.resolve(paths.templates, 'function.js'), 110 | 'utf-8' 111 | ) 112 | 113 | // Avoid any conflicts if function output directory already exists. 114 | if (await exists(paths.functions)) { 115 | throw new Error( 116 | `Function output directory "${paths.functions}" already exists` 117 | ) 118 | } 119 | 120 | // prepare output dir. 121 | await fs.mkdir(paths.functions, { recursive: true }) 122 | 123 | log.info(`Created output dir: "${path.relative(paths.cwd, paths.functions)}"`) 124 | 125 | const redirects = (await exists(paths.redirects)) 126 | ? [await fs.readFile(paths.redirects, 'utf-8')] 127 | : [] 128 | 129 | const apiRoutes = await getAPIRoutes(paths) 130 | log.info(`Resolved ${apiRoutes.length} API routes:`) 131 | 132 | if (apiRoutes.length) { 133 | redirects.push('# Next Export API redirects') 134 | } 135 | 136 | for (const [url, relative] of apiRoutes) { 137 | const filename = url.replace(/\//g, '__') 138 | const destination = path.resolve(paths.functions, `${filename}.js`) 139 | 140 | // create netlify function file. 141 | const content = template.replace('%next-api-route%', relative) 142 | await fs.writeFile(destination, content) 143 | 144 | // add redirect rule 145 | redirects.push(`${url} /.netlify/functions/${filename} 200`) 146 | 147 | log.success(`- Created ${url} route`) 148 | } 149 | 150 | await fs.writeFile(paths.redirects, redirects.join('\n'), 'utf-8') 151 | log.success('Created redirects') 152 | } 153 | 154 | run().catch(log.fatal) 155 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adaptor' 2 | -------------------------------------------------------------------------------- /templates/function.js: -------------------------------------------------------------------------------- 1 | const { adaptor } = require('next-export-api') 2 | const { default: handler } = require('%next-api-route%') 3 | 4 | module.exports.handler = adaptor(handler) 5 | -------------------------------------------------------------------------------- /test/adaptor.test.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiHandler } from 'next' 2 | import { adaptor } from '../src/adaptor' 3 | 4 | describe('adaptor', () => { 5 | const handlers: { [key: string]: NextApiHandler } = { 6 | ok: (req, res) => res.send('ok'), 7 | status: (req, res) => res.status(201).send('ok'), 8 | query: (req, res) => res.send(req.query.name), 9 | post: (req, res) => res.send('ok'), 10 | body: (req, res) => res.send(req.body.name), 11 | json: (req, res) => res.json({ message: 'ok' }), 12 | } 13 | 14 | const adaptors = { 15 | ok: adaptor(handlers.ok), 16 | status: adaptor(handlers.status), 17 | query: adaptor(handlers.query), 18 | post: adaptor(handlers.post), 19 | body: adaptor(handlers.body), 20 | json: adaptor(handlers.json), 21 | } 22 | 23 | beforeEach(jest.resetModules) 24 | 25 | describe('Netlify', () => { 26 | const { ok, status, query, post, body, json } = adaptors 27 | 28 | test('"ok" handler', async () => { 29 | const result = await ok( 30 | { httpMethod: 'GET', path: '/' } as any, 31 | {} as any 32 | ) 33 | 34 | expect(result.body).toEqual('ok') 35 | }) 36 | 37 | test('"status" handler', async () => { 38 | const result = await status( 39 | { httpMethod: 'GET', path: '/' } as any, 40 | {} as any 41 | ) 42 | 43 | expect(result.statusCode).toEqual(201) 44 | expect(result.body).toEqual('ok') 45 | }) 46 | 47 | test('"query" handler', async () => { 48 | const result = await query( 49 | { 50 | httpMethod: 'GET', 51 | path: '/', 52 | queryStringParameters: { name: 'susan' }, 53 | } as any, 54 | {} as any 55 | ) 56 | 57 | expect(result.body).toEqual('susan') 58 | }) 59 | 60 | test('"post" handler', async () => { 61 | const result = await post( 62 | { httpMethod: 'POST', path: '/' } as any, 63 | {} as any 64 | ) 65 | 66 | expect(result.body).toEqual('ok') 67 | }) 68 | 69 | test('"body" handler', async () => { 70 | const result = await body( 71 | { 72 | httpMethod: 'POST', 73 | path: '/', 74 | headers: { 'Content-Type': 'application/json' }, 75 | body: '{ "name": "susan" }', 76 | } as any, 77 | {} as any 78 | ) 79 | 80 | expect(result.body).toEqual('susan') 81 | }) 82 | 83 | test('"json" handler', async () => { 84 | const result = await json( 85 | { httpMethod: 'GET', path: '/' } as any, 86 | {} as any 87 | ) 88 | 89 | expect(result).toHaveProperty( 90 | 'headers.content-type', 91 | expect.stringMatching('application/json') 92 | ) 93 | 94 | expect(result.body).toEqual('{"message":"ok"}') 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "declaration": true, 5 | "target": "es6", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": true, 14 | "pretty": true, 15 | "lib": ["esnext"] 16 | }, 17 | "include": ["src/**/*", "test/**/*"] 18 | } 19 | --------------------------------------------------------------------------------