Wednesday, October 25, 2023

Show HN: Instant API – Build type-safe web APIs with JavaScript

travis-ci build npm version

Instant API is a framework for building APIs with JavaScript that implements type-safety at the HTTP interface. By doing so, it eliminates the need for schema validation libraries entirely. Simply write a JSDoc-compliant comment block for a function that represents your API endpoint and stop worrying about validation, sanitization and testing for user input. The OpenAPI specification for your API is then automatically generated in both JSON and YAML at localhost:8000/.well-known/openapi.json and localhost:8000/.well-known/openapi.yaml, respectively.

Additionally, Instant API comes a number of features optimized for integrations with LLMs and chat bots:

  • First class support for Server-Sent Events using text/event-stream makes streaming LLM responses easy
  • LLM function calling can be integrated easily via JSON schema output at localhost:8000/.well-known/schema.json
  • Experimental auto-generation of localhost:8000/.well-known/ai-plugin.json
  • The ability to instantly return 200 OK responses and execute in the background for Slack, Discord webhooks

You will find Instant API is a very full-featured framework despite being an early release. It has been in development for six years as the engine behind the Autocode serverless platform where it has horizontally scaled to handle over 100M API requests per day.

Here's an example API endpoint built with Instant API. It would be available at the URL example.com/v1/weather/current via HTTP GET. It has length restrictions on location, range restrictions on coords.lat and coords.lng, and tags is an array of string. The @returns definitions ensure that the API contract with the user is upheld: if the wrong data is returned an error will be thrown.

File: /functions/v1/weather/current.mjs

/**
 * Retrieve the weather for a specific location
 * @param {?string{1..64}} location   Search by location
 * @param {?object}        coords     Provide specific latitude and longitude
 * @param {number{-90,90}} coords.lat Latitude
 * @param {number{-90,90}} coords.lng Longitude
 * @param {string[]}       tags       Nearby locations to include
 * @returns {object} weather             Your weather result
 * @returns {number} weather.temperature Current tmperature of the location
 * @returns {string} weather.unit        Fahrenheit or Celsius
 */
export async function GET (location = null, coords = null, tags = []) {

  if (!location && !coords) {
    // Prefixing an error message with a "###:" between 400 and 404
    //   automatically creates the correct client error:
    //     BadRequestError, UnauthorizedError, PaymentRequiredError,
    //     ForbiddenError, NotFoundError
    // Otherwise, will throw a RuntimeError with code 420
    throw new Error(`400: Must provide either location or coords`);
  } else if (location && coords) {
    throw new Error(`400: Can not provide both location and coords`);
  }

  // Fetch your own API data
  await getSomeWeatherDataFor(location, coords, tags);

  // mock a response
  return {
    temperature: 89.2
    units: `°F`
  };

}

LLM streaming is simple. It relies on a special context object and defining @stream parameters to create a text/event-stream response. You can think of @stream as similar to @returns, where you're specifying the schema for the output to the user. If this contract is broken, your API will throw an error. In order to send a stream to the user, we add a special context object to the API footprint as the last parameter and use an exposed context.stream() method.

File: /functions/v1/ai-helper.mjs

import OpenAI from 'openai';
const openai = new OpenAI(process.env.OPENAI_API_KEY);

/**
 * Streams results for our lovable assistant
 * @param {string} query The question for our assistant
 * @stream {object}   chunk
 * @stream {string}   chunk.id
 * @stream {string}   chunk.object
 * @stream {integer}  chunk.created
 * @stream {string}   chunk.model
 * @stream {object[]} chunk.choices
 * @stream {integer}  chunk.choices[].index
 * @stream {object}   chunk.choices[].delta
 * @stream {?string}  chunk.choices[].delta.role
 * @stream {?string}  chunk.choices[].delta.content
 * @returns {object} message
 * @returns {string} message.content
 */
export async function GET (query, context) {
  const completion = await openai.chat.completions.create({
    messages: [
      {role: `system`, content: `You are a lovable, cute assistant that uses too many emojis.`},
      {role: `user`, content: query}
    ],
    model: `gpt-3.5-turbo`,
    stream: true
  });
  const messages = [];
  for await (const chunk of completion) {
    // Stream our response as text/event-stream when ?_stream parameter added
    context.stream('chunk', chunk); // chunk has the schema provided above
    messages.push(chunk?.choices?.[0]?.delta?.content || '');
  }
  return {content: messages.join('')};
};

By default, this method will return something like;

{
  "content": "Hey there! 💁‍♀️ I'm doing great, thank you! 💖✨ How about you? 😊🌈"
}

However, if you append ?_stream to query parameters or {"_stream": true} to body parameters, it will turn into a text/event-stream with your context.stream() events sandwiched between a @begin and @response event. The @response event will be an object containing the details of what the HTTP response would have contained had the API call been made normally.

id: 2023-10-25T04:29:59.115000000Z/2e7c7860-4a66-4824-98fa-a7cf71946f19
event: @begin
data: "2023-10-25T04:29:59.115Z"

[... more events ...]

event: chunk
data: {"id":"chatcmpl-8DPoluIgN4TDIuE1usFOKTLPiIUbQ","object":"chat.completion.chunk","created":1698208199,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" 💯"},"finish_reason":null}]}

[... more events ...]

event: @response
data: {"statusCode":200,"headers":{"X-Execution-Uuid":"2e7c7860-4a66-4824-98fa-a7cf71946f19","X-Instant-Api":"true","Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, OPTIONS, HEAD, PUT, DELETE","Access-Control-Allow-Headers":"","Access-Control-Expose-Headers":"x-execution-uuid, x-instant-api, access-control-allow-origin, access-control-allow-methods, access-control-allow-headers, x-execution-uuid","Content-Type":"application/json"},"body":"{\"content\":\"Hey there! 🌞 I'm feeling 💯 today! Full of energy and ready to help you out. How about you? How are you doing? 🌈😊\"}"}
  1. Getting Started
    1. Quickstart
    2. Custom installation
  2. Endpoints and Type Safety
    1. Creating Endpoints
    2. Responding to HTTP methods
      1. Endpoint lifecycle
      2. Typing your endpoint
        1. Undocumented parameters
        2. Required parameters
        3. Optional parameters
      3. context object
      4. API endpoints: functions/ directory
        1. Index routing with index.mjs
        2. Subdirectory routing with 404.mjs
      5. Static files: www/ directory
        1. Index routing with index.html
        2. Subdirectory routing with 404.html
    3. Type Safety
      1. Supported types
      2. Type coercion
      3. Combining types
      4. Enums and restricting to specific values
      5. Sizes (lengths)
      6. Ranges
      7. Arrays
      8. Object schemas
    4. Parameter validation
      1. Query and Body parsing with application/x-www-form-urlencoded
      2. Query vs. Body parameters
    5. CORS (Cross-Origin Resource Sharing)
    6. Returning responses
      1. @returns type safety
      2. Error responses
      3. Custom HTTP responses
      4. Returning files with Buffer responses
      5. Streaming responses
      6. Debug responses
    7. Throwing errors
  3. OpenAPI Specification Generation
    1. OpenAPI Output Example
    2. JSON Schema Output Example
    3. Hiding endpoints with @private
  4. Streaming and LLM Support
    1. @stream type safety
    2. Using context.stream()
    3. Using the _stream parameter
      1. Selectively listening to specific streams
  5. Background execution for webhooks and chatbots
    1. @background directive
    2. Using the _background parameter
    3. Background modes
  6. Debugging
    1. Using context.log() and context.error()
    2. Using the _debug parameter
  7. Built-in Errors
  8. Testing
    1. Quickstart for tests
    2. Custom installation for tests
    3. Writing tests
    4. Running tests
  9. Deployment
    1. via instant deploy
    2. Custom deployments
  10. More Information
    1. Logging
    2. Error monitoring
    3. Middleware
  11. Roadmap and Feedback
  12. Acknowledgements

The quickest way to get started with Instant API is via the instant.dev command line tools. It is the easiest way to get your Instant API project set up, generate new endpoints, manage tests and comes with built-in deployment tooling for Vercel or AWS. It comes packaged with the Instant ORM which makes setting up a Postgres-based backend a breeze.

npm i instant.dev -g
cd ~/projects
mkdir my-new-project
cd my-new-project
instant init

From there, you can use more advanced features:

# Create an endpoint (test generated automatically)
instant g:endpoint first_endpoint

# Run your server
instant serve

# Run tests
instant test

# See all available commands
instant help

Note: Most of this documentation will assume you are using the instant.dev CLI. It is the recommended get your Instant API project set up, generate new endpoints, manage tests and comes with built-in deployment tooling for Vercel or AWS.

To use Instant API without the command line tools, you can do the following;

cd path/to/my/project
npm i @instant.dev/api --save

Then add the following to package.json:

  "scripts": {
    "start": "node instant.mjs"
  },

And copy the following file to instant.mjs:

// Third-party imports
import InstantAPI from '@instant.dev/api';

// Native imports
import cluster from 'cluster';
import os from 'os';

// Shorthand references
const Daemon = InstantAPI.Daemon;
const Gateway = InstantAPI.Daemon.Gateway;
const EncryptionTools = InstantAPI.EncryptionTools;

// Constants
const ENVIRONMENT = process.env.NODE_ENV || 'development';
const PORT = process.env.PORT || 8000;

if (cluster.isPrimary) {

  // Multi-process daemon
  const daemon = new Daemon(
    ENVIRONMENT !== 'development'
      ? os.cpus().length
      : 1
  );
  daemon.start(PORT);

} else {

  // Individual webserver startup
  const gateway = new Gateway({debug: ENVIRONMENT !== 'production'});
  gateway.load(process.cwd());       // load routes from filesystem
  gateway.listen(PORT);              // start server

}

To start your server, simply run:

Instant API relies on a Function as a Service model for endpoint execution: every {Route, HTTP Method} combination is modeled as an exported function. To add parameter validation a.k.a. type safety to your endpoints, you simply document your exported functions with a slightly modified JSDoc specification comment block. For example, the simplest endpoint possible would look like this;

File: functions/index.js

export default async function () {
  return `hello world`;
}

And you could execute it with;

curl localhost:8000/
> "hello world"

Assuming you are running instant serve or npm start on port 8000. See Getting started for more details on starting your server.

The easiest way to create endpoints for Instant API is via the instant.dev command line tools. Once your project has been initialized, you can write:

instant g:endpoint path/to/endpoint

And voila! A new blank endpoint has been created at /functions/path/to/endpoint/index.mjs.

If you want to create an endpoint manually, just create a new .mjs file in the functions/ directory and make sure it outputs a function corresponding to at least one HTTP method: GET, POST, PUT or DELETE.

In the example above, we used export default to export a default function. This function will respond to to all GET, POST, PUT and DELETE requests with the same function. Alternatively, we can export functions for each method individually, like so:

File: functions/index.js

export async function GET () {
  return `this was a GET request!`;
}

export async function POST () {
  return `this was a POST request!`;
}

Any method not specified in this manner will automatically return an HTTP 501 error (Not Implemented). You can test these endpoints like so;

curl -X GET localhost:8000/
> "this was a GET request!"

curl -X POST localhost:8000/
> "this was a POST request!"

curl -X PUT localhost:8000/
> {"error":...} # Returns NotImplementedError (501)

Note: Method names are case sensitive, they must be uppercase. Instant API will throw an error if the exports aren't read properly.

When endpoint files, like functions/index.js above, are accessed they are imported only once per process. Code outside of export statements is executed lazily the first time the function is executed. By default, in a production server environment, Instant API will start one process per virtual core.

Each exported function will be executed every time it is called. For the most part, you should only use the area external to your export statements for critical library imports and frequently accessed object caching; not for data persistence.

Here's an example using Instant ORM:

// DO THIS: Cache connections and commonly used objects, constructors

// Executed only once per process: lazily on first execution
import InstantORM from '@instant.dev/orm';
const Instant = await InstantORM.connectToPool(); // connect to postgres
const User = Instant.Model('User'); // access User model

/**
 * Find all users matching a provided username
 * @param {string} searchQuery Username portion to search for
 * @returns {object[]} users
 */
export async function GET (searchQuery) {
  // Executed each time endpoint called
  return await User.query()
    .where({username__icontains: searchUsername})
    .select();
}

Here's an example of what you should not do:

// DO NOT DO THIS: data persistence not reliable in production workloads
//    Could be on a multi-core server or serverless deployment e.g. Vercel

let pageViews = 0;

/**
 * Track views -- poorly. Persistence unreliable!
 */
export async function GET () {
  return `This page has been viewed ${++pageViews} times`;
}

Endpoints can be typed using a slightly modified JSDoc specification that is easily interpreted and syntax highlighted by most modern code editors. You type your endpoint by (1) providing a comment block immediately preceding your exported function and / or (2) providing default values for your exported function.

Note: Parameter documentation for typing is an all-or-none affair. Instant API will refuse to start up if documented endpoints do not match the function signature.

By default, if you do not document your parameters at all, they will be assumed to be type any and all be required. If you provided default values, the parameters will be optional but will assume the type of their default value.

export async function GET (name, age = 25) {
  return `hello ${name} you are ${age}`;
}

In this case, name is required by can take on any type. age is optional but must be a number.

curl -X GET localhost:8000/
> {"error":...} # Returns ParameterError (400) -- name is required

curl -X GET localhost:8000/?name=world
> "hello world you are 25"

curl -X GET 'localhost:8000/?name=world&age=lol'
> {"error":...} # Returns ParameterError (400) -- age should be a number

curl -X GET 'localhost:8000/?name=world&age=99'
> "hello world you are 99"

A parameter is required if you do not provide a default value in the function signature. For example;

/**
 * @param {string} name 
 */
export async function GET (name) {
  return `hello ${name}`;
}

Will return a ParameterError with status code 400 indicating the name parameter is required if no name is passed in to the endpoint.

curl -X GET localhost:8000/
> {"error":...} # Returns ParameterError (400)

curl -X GET localhost:8000/?name=world
> "hello world"

A parameter is optional if you:

  • prefix the type with a ?
  • AND / OR provide a default value in the function signature

If you prefix the type with ?, the default value is assumed to be null. You can not use undefined as an acceptable endpoint parameter value.

For example;

/**
 * @param {?string} name 
 * @param {number} age
 */
export async function GET (name, age = 4.2e9) {
  return `hello ${name}, you are ${age}`;
}

Even though name was not provided in the function signature, the ? in {?string} indicates that this parameter is optional. It will be given a default value of null. When included in a template string, null will be printed as the string "null".

curl -X GET localhost:8000/
> "hello null, you are 4200000000"

curl -X GET localhost:8000/?name=world
> "hello world, you are 4200000000"

curl -X GET 'localhost:8000/?name=world&age=101'
> "hello world, you are 101"

The context object is a "magic" parameter that can be appended to any function signature. It can not be documented and you can not use "context" as a parameter name. The other magic parameters are _stream (Streaming and LLM support), _debug (Debug) and _background. However, only context can be added to your function signature.

You can use context to access execution-specific information like so;

export async function GET (context) {
  console.log(context.http.method);   // "GET"
  console.log(context.http.body);     // Request body (utf8)
  console.log(context.remoteAddress); // IP address
  return context;                     // ... and much more
}

It also comes with a context.stream() function which you can read about in Streaming and LLM support.

A full list of available properties is as follows;

{
  "name": "endpoint_name",
  "alias": "request_pathname",
  "path": ["request_pathname", "split", "by", "/"],
  "params": {"jsonified": "params", "passed": "via_query_and_body"},
  "remoteAddress": "ipv4_or_v6_address",
  "uuid": "request_uuid",
  "http": {
    "url": "request_url",
    "method": "request_method",
    "headers": {"request": "headers"},
    "body": "request_body_utf8",
    "json": "request_body_json_if_applicable_else_null",
  },
  "stream": function stream (name, value) { /* ... */ }
}

By default, anything in the root functions/ directory of an Instant API project is exported as an API endpoint. All .js, .cjs and .mjs files are valid. We have not covered CommonJS-styled exports here as they are supported for legacy purposes and not recommended for forward-facing development.

Routing is handled by mapping the pathname of an HTTP request to the internal file pathname of the function, not including functions/ or the file extension. For example, an HTTP request to /v1/hello-world will trigger functions/v1/hello-world.js.

There are four "magic" filenames that can be used to handle indices or subdirectories. index.mjs / __main__.mjs will act as a handler for the root directory and 404.mjs / __notfound__.mjs will act as a handler for any subdirectory or file that is not otherwise defined.

Alias: __main__.mjs

Handler for the root directory. For example, functions/v1/stuff/index.mjs is accessible via /v1/stuff.

Alias: __notfound__.mjs

Handler for subdirectories not otherwise defined. For example, with the following directory structure:

- functions/
  - v1/
    - stuff/
      - 404.mjs
      - abc.mjs

The following HTTP request pathnames would map to these endpoints;

  • /v1/stuff -> functions/v1/stuff/404.mjs
  • /v1/stuff/abc -> functions/v1/stuff/abc.mjs
  • /v1/stuff/abcd -> functions/v1/stuff/404.mjs
  • /v1/stuff/abc/def -> functions/v1/stuff/404.mjs

You can use this behavior to define custom routing schemes. If you want custom 404 error pages we recommend using Subdirectory routing with 404.html instead.

Instant API comes with built-in static file hosting support. Instead of putting files in the functions/ directory, put any file you want in the www/ directory to automatically have it hosted as a standalone static file.

The rules for static hosting are as follows;

  • The server root / maps directly to www/
  • e.g. /image.png -> www/image.png
  • All .htm and .html files will be available with AND without suffixes
  • /hello -> www/hello.html
  • /hello.html -> www/hello.html
  • API and static routes can not conflict
  • functions/wat.mjs would overlap with www/wat.htm and is not allowed

There are four "magic" filenames that can be used to handle indices or subdirectories. index.html / index.htm will act as a handler for the root directory and 404.html / 404.htm will act as a handler for any subdirectory or file that is not otherwise defined.

Alias: index.htm

Same behavior as index.js for API routes. Handler for the root pathname.

Alias: 404.htm

Same behavior as 404.js for API routes. Handler for subdirectories that are not otherwise defined. Ideal use case is for custom 404 error pages.

Types are applied to parameter and schema validation based upon the comment block preceding your exported endpoint function.

/**
 * My GET endpoint
 * @param {any} myparam
 */
export async function GET (myparam) {
  // do something with myparam
}
Type Definition Example Parameter Input Values (JSON)
boolean True or False true or false
string Basic text or character strings "hello", "GOODBYE!"
number Any double-precision Floating Point value 2e+100, 1.02, -5
float Alias for number 2e+100, 1.02, -5
integer Subset of number, integers between -2^53 + 1 and +2^53 - 1 (inclusive) 0, -5, 2000
object Any JSON-serializable Object {}, {"a":true}, {"hello":["world"]}
object.http An object representing an HTTP Response. Accepts headers, body and statusCode keys {"body": "Hello World"}, {"statusCode": 404, "body": "not found"}, {"headers": {"Content-Type": "image/png"}, "body": Buffer.from(...)}
array Any JSON-serializable Array [], [1, 2, 3], [{"a":true}, null, 5]
buffer Raw binary octet (byte) data representing a file. {"_bytes": [8, 255]} or {"_base64": "d2h5IGRpZCB5b3UgcGFyc2UgdGhpcz8/"}
any Any value mentioned above 5, "hello", []

The buffer type will automatically be converted to a Buffer from any object with a single key-value pair matching the footprints {"_bytes": []} or {"_base64": ""}.

Otherwise, parameters provided to a function are expected to match their defined types. Requests made over HTTP GET via query parameters or POST data with type application/x-www-form-urlencoded will be automatically converted from strings to their respective expected types, when possible.

Once converted, all types will undergo a final type validation. For example, passing a JSON array like ["one", "two"] to a parameter that expects an object will convert from string to JSON successfully but fail the object type check.

Type Conversion Rule
boolean "t" and "true" become true, "f" and "false" become false, otherwise will be kept as string
string No conversion: already a string
number Determine float value, if NaN keep as string, otherwise convert
float Determine float value, if NaN keep as string, otherwise convert
integer Determine float value, if NaN keep as string, otherwise convert: may fail integer check
object Parse as JSON, if invalid keep as string, otherwise convert: may fail object check
object.http Parse as JSON, if invalid keep as string, otherwise convert: may fail object.http check
array Parse as JSON, if invalid keep as string, otherwise convert: may fail array check
buffer Parse as JSON, if invalid keep as string, otherwise convert: may fail buffer check
any No conversion: keep as string

You can combine types using the pipe | operator. For example;

/**
 * @param {string|integer} myparam String or an integer
 */
export async function GET (myparam) {
  // do something
} 

Will accept a string or an integer. Types defined this way will validate against the provided types in order of appearance. In this case, since it is a GET request and all parameters are passed in as strings via query parameters, myparam will always be received a string because it will successfully pass the string type coercion and validation first.

However, if you use a POST request:

/**
 * @param {string|integer} myparam String or an integer
 */
export async function POST (myparam) {
  // do something
} 

Then you can pass in {"myparam": "1"} or {"myparam": 1} via the body which would both pass type validation.

You can combine as many types as you'd like:

@param {string|buffer|array|integer}

Including any in your list will, as expected, override any other type specifications.

Similar to combining types, you can also include specific JSON values in your type definitions:

/**
 * @param {"one"|"two"|"three"|4} myparam String or an integer
 */
export async function GET (myparam) {
  // do something
} 

This allows you to restrict possible inputs to a list of allowed values. In the case above, sending ?myparam=4 via HTTP GET will successfully parse to 4 (Number), because it will fail validation against the three string options.

You can combine specific values and types in your definitions freely:

@param {"one"|"two"|integer}

Just note that certain combinations will invalidate other list items. Like {1|2|integer} will accept any valid integer.

The types string, array and buffer support sizes (lengths) via the {a..b} modifier on the type. For example;

@param {string{..9}}  alpha
@param {string{2..6}} beta
@param {string{5..}}  gamma

Would expect alpha to have a maximum length of 9, beta to have a minimum length of 2 but a maximum length of 6, and gamma to have a minimum length of 5.

The types number, float and integer support ranges via the {a,b} modifier on the type. For example;

@param {number{,1.2e9}} alpha
@param {number{-10,10}} beta
@param {number{0.870,}} gamma

Would expect alpha to have a maximum value of 1 200 000 000, beta to have a minimum value of -10 but a maximum value of 10, and gamma to have a minimum value of 0.87.

Arrays are supported via the array type. You can optionally specify a schema for the array which applies to every element in the array. There are two formats for specifying array schemas, you can pick which works best for you:

@param {string[]}      arrayOfStrings1
@param {array<string>} arrayOfStrings2

For multi-dimensional arrays, you can use nesting:

@param {integer[][]}           array2d
@param {array<array<integer>>} array2d_too

Please note: Combining types are not currently available in array schemas. Open up an issue and let us know if you'd like them and what your use case is! In the meantime;

@param {integer[]|string[]}

Would successfully define an array of integers or an array of strings.

To define object schemas, use the subsequent lines of the schema after your initial object definition to define individual properties. For example, the object {"a": 1, "b": "two", "c": {"d": true, "e": []} Could be defined like so:

@param {object}  myObject
@param {integer} myObject.a
@param {string}  myObject.b
@param {object}  myObject.c
@param {boolean} myObject.c.d
@param {array}   myObject.c.e

To define object schemas that are members of arrays, you must identify the array component in the property name with []. For example:

@param {object[]} topLevelArray
@param {integer}  topLevelArray[].value
@param {object}   myObject
@param {object[]} myObject.subArray
@param {string}   myObject.subArray[].name

Parameter validation occurs based on types as defined per Type Safety. The process for parameter validation takes the following steps:

  1. Read parameters from the HTTP query string as type application/x-www-form-urlencoded
  2. If applicable, read parameters from the HTTP body based on the request Content-Type
    • Supported content types:
      • application/json
      • application/x-www-formurlencoded
      • multipart/form-data
      • application/xml, application/atom+xml, text/xml
  3. Query parameters can not conflict with body parameters, throw an error if they do
  4. Perform type coercion on application/x-www-form-urlencoded inputs (query and body, if applicable)
  5. Validate parameters against their expected types, throw an error if they do not match

During this process, you can encounter a ParameterParseError or a ParameterError both with status code 400. ParameterParseError means your parameters could not be parsed based on the expected or provided content type, and ParameterError is a validation error against the schema for your endpoint.

Many different standards have been implemented and adopted over the years for HTTP query parameters and how they can be used to specify objects and arrays. To make things easy, Instant API supports all common query parameter parsing formats.

Here are some query parameter examples of parsing form-urlencoded data:

  • Arrays
    • Duplicates: ?arr=1&arr=2 becomes [1, 2]
    • Array syntax: ?arr[]=1&arr[]=2 becomes [1, 2]
    • Index syntax: ?arr[0]=1&arr[2]=3 becomes [1, null, 3]
    • JSON syntax: ?arr=[1,2] becomes [1, 2]
  • Objects
    • Bracket syntax: ?obj[a]=1&obj[b]=2 becomes {"a": 1, "b": 2}
    • Dot syntax: ?obj.a=1&obj.b=2 becomes {"a": 1, "b": 2}
      • Nesting: ?obj.a.b.c.d=t becomes {"a": {"b": {"c": {"d": true}}}}
    • JSON syntax: ?obj={"a":1,"b":2} becomes {"a": 1, "b": 2}

With Instant API, query and body parameters can be used interchangeably. The general expectation is that POST and PUT endpoints should typically only interface with the content body, but API consumers should be able to freely manipulate query parameters if they want to play around. For example, the endpoint defined by:

/**
 * Hello world endpoint
 * @param {string} name
 * @param {number} age
 */
export async function POST (name, age) {
  return `hello ${name}, you are ${age}!`;
}

Could be triggered successfull via;

curl -X POST 'localhost:8000/hello-world?name=world&age=99'
curl -X POST 'localhost:8000/hello-world?name=world' --data '{"age":99}'
curl -X POST 'localhost:8000/hello-world' --data '{"name":"world","age":99}'

Generally speaking, our motivation for this pattern comes from two observations;

  1. In decades of software development we have never seen a legitimate use case for query parameters and body parameters with the same name on a single endpoint
  2. Exposing APIs this way is a lot easier for end users to play with

To prevent unexpected errors, naming collisions will throw an error at the gateway layer, before your endpoint is executed.

By default, all endpoints have a completely open CORS policy, they all return the header Access-Control-Allow-Origin: *.

To restrict endpoints to specific URLs use the @origin directive. You can add as many of these as you'd like.

/**
 * My CORS-restricted endpoint
 * @origin staging.my-website.com
 * @origin http://localhost:8000
 * @origin https://my-website.com
 * @origin =process.env.ALLOWED_ORIGIN
 * @origin =process.env.ANOTHER_ALLOWED_ORIGIN
 * @param {number} age
 */
export async function POST (name, age) {
  return `hello ${name}, you are ${age}!`;
}

The CORS Access-Control-Allow-Origin policy will be set like so;

  • If no protocol is specified, allow all traffic from the URL
  • If port is specified, only allow traffic from the URL on the specified port
  • If http:// is specified, only allow http protocol traffic from the URL
  • If https:// is specified, only allow https protocol traffic from the URL
  • If origin starts with =process.env., it will rely on the specified environment variable

Note that =process.env.ENV_NAME entries will be loaded at startup time. Dynamically changing process.env afterwards will have no effect on your allowed origins.

Returning API responses from your endpoint is easy. Just add a return statement with whatever data you would like to return.

export async function GET () {
  return `hello world`; // works as expected
}

By default, all responses will be JSON.stringify()-ed and returned with the Content-Type header set to application/json.

curl localhost:8000/hello-world
> "hello world"

There are two exceptions: returning an object.http object (containing statusCode, headers, and body) allows you to provide a Custom HTTP response and returning a Buffer, which are treated as raw binary (file) data.

Similar to Parameter validation, you can enforce a type schema on the return value of your endpoint like so;

/**
 * @returns {object} message
 * @returns {string} message.content
 */
export async function GET () {
  return {message: `hello world`};
}

The difference between @returns type safety as compared to @param validation is that this type safety mechanism is run after your code has been executed. If you fail a @returns type safety check, the user receives a ValueError with status code 502: a server error. The function may have executed successfully but the value does not fulfill the promised API contract. This functionality exists to ensure users can trust the type contract of your API. To avoid production snafus, we recommend writing tests to validate that your endpoints return the values you expect them to.

Any uncaught promises or thrown errors will result in a RuntimeError with a status code of 420 (unknown) by default. To customize error codes, check out Throwing Errors.

To return a custom HTTP response, simply return an object with one or all of the following keys: statusCode (integer), headers (object) and body (Buffer). You can specify this in the @returns schema, however Instant API will automatically detect the type.

/**
 * I'm a teapot
 */
export async function GET () {
  return {
    statusCode: 418,
    headers: {'Content-Type', 'text/plain'},
    body: Buffer.from(`I'm a teapot!`)
  };
}

If you would like to return a raw file from the file system, compose binary data into a downloadable file, or dynamically generate an image (e.g. with Dall-E or Stable Diffusion) you can build a custom HTTP response as per above - but Instant API makes it a little easier than that.

If you return a Buffer object you can optionally specify a contentType to set the Content-Type http header like so:

import fs from 'fs';

/**
 * Return an image from the filesystem to be displayed
 */
export async function GET () {
  const buffer = fs.readFileSync('./path/to/image.png');
  buffer.contentType = 'image/png';
  return buffer;
}

Instant API has first-class support for streaming using the text/event-stream content type and the "magic" _stream parameter. You can read more in Streaming and LLM support.

In development environments, e.g. when process.env.NODE_ENV=development, you can stream the results of any function using the "magic" _debug parameter. This allows you to monitor function execution in the browser for long-running jobs. You can read more in Debugging.

Whenever a throw statement is executed or a Promise is uncaught within the context of an Instant API endpoint, the default behavior is to return a RuntimeError with a status code of 420: your browser will refer to this as "unknown", we think of it as "confused".

To specify a specific error code between 400 and 404, simply throw an error prefixed with the code and a colon like so:

/**
 * Errors out
 */
export async function GET () {
  throw new Error(`400: No good!`);
}

When you execute this function you would see a BadRequestError with a status code of 400:

{
  "error": {
    "type": "BadRequestError",
    "message": "No good!"
  }
}

The following error codes will automatically map to error types:

  • 400: BadRequestError
  • 401: UnauthorizedError
  • 402: PaymentRequiredError
  • 403: ForbiddenError
  • 404: NotFoundError

OpenAPI specifications are extremely helpful for machine-readability but are extremely verbose. Instant API allows you to manage all your type signatures and parameter validation via JSDoc in a very terse manner while automatically generating your OpenAPI specification for you. By default Instant API will create three schema files based on your API:

  • localhost:8000/.well-known/openapi.json
  • localhost:8000/.well-known/openapi.yaml
  • localhost:8000/.well-known/schema.json

The first two are OpenAPI schemas and the final one is a JSON schema which outputs a {"functions": [...]} object. The latter is primarily intended for use with OpenAI function calling and other LLM integrations.

As a simple example, consider the following endpoint:

/**
 * Gets a "Hello World" message
 * @param {string} name
 * @param {number{12,199}} age
 * @returns {string} message
 */
export async function GET (name, age) {
  return `hello ${name}, you are ${age} and you rock!`
}

/**
 * Creates a new hello world message
 * @param {object} body
 * @param {string} body.content
 * @returns {object}  result
 * @returns {boolean} result.created
 */
export async function POST (body) {
  console.log(`Create body ... `, body);
  return {created: true};
}

Once saved as part of your project, if you open localhost:8000/.well-known/openapi.yaml in your browser, you should receive the following OpenAPI specification:

openapi: "3.1.0"
info:
  version: "development"
  title: "(No name provided)"
  description: "(No description provided)"
servers:
  - url: "localhost"
    description: "Instant API Gateway"
paths:
  /hello-world/:
    get:
      summary: "Gets a \"Hello World\" message"
      description: "Gets a \"Hello World\" message"
      operationId: "service_localhost_hello_world_get"
      parameters:
        - in: "query"
          name: "name"
          schema:
            type: "string"
        - in: "query"
          name: "age"
          schema:
            type: "number"
            minimum: 12
            maximum: 199
      responses:
        200:
          content:
            application/json:
              schema:
                type: "string"
    post:
      summary: "Creates a new hello world message"
      description: "Creates a new hello world message"
      operationId: "service_localhost_hello_world_post"
      requestBody:
        content:
          application/json:
            schema:
              type: "object"
              properties:
                body:
                  type: "object"
                  properties:
                    content:
                      type: "string"
                  required:
                    - "content"
              required:
                - "body"
      responses:
        200:
          content:
            application/json:
              schema:
                type: "object"
                properties:
                  created:
                    type: "boolean"
                required:
                  - "created"

Using the same endpoint defined above would produce the following JSON schema at localhost:8000/.well-known/schema.json:

{
  "functions": [
    {
      "name": "hello-world_get",
      "description": "Gets a \"Hello World\" message",
      "route": "/hello-world/",
      "url": "localhost/hello-world/",
      "method": "GET",
      "parameters": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "age": {
            "type": "number",
            "minimum": 12,
            "maximum": 199
          }
        },
        "required": [
          "name",
          "age"
        ]
      }
    },
    {
      "name": "hello-world",
      "description": "Creates a new hello world message",
      "route": "/hello-world/",
      "url": "localhost/hello-world/",
      "method": "POST",
      "parameters": {
        "type": "object",
        "properties": {
          "body": {
            "type": "object",
            "properties": {
              "content": {
                "type": "string"
              }
            },
            "required": [
              "content"
            ]
          }
        },
        "required": [
          "body"
        ]
      }
    }
  ]
}

Don't want all of your endpoints exposed to your end users? No problem. Simply mark an endpoint as @private, like so:

/**
 * My admin function
 * @private
 */
export async function POST (context) {
  await authenticateAdminUser(context);
  doSomethingAdministrative();
  return `ok!`;
}

This will prevent it from being shown in either your OpenAPI or JSON schema outputs.

Instant API comes with built-in support for streaming Server-Sent Events with the text/event-stream content type. This allows you to send events to a user as they are received, and is ideal for developing LLM-based APIs.

Streams are typed events that can be sent via a special context.stream() method. They must be defined using the @stream directive in the JSDoc descriptor for your API endpoint. Streams are typed like @param and @returns, and these types are enforced: if you choose to context.stream(name, payload) the wrong data format in payload, your function will throw an error.

By default, functions will not stream even when a @stream is defined and context.stream() is called. They will simply accept parameters and output a returned response. In order to activate streaming, you must pass a _stream parameter in the HTTP query parameters or body content - this will initiate a Server-Sent Event with content type text/event-stream.

An example of a simple custom streaming LLM agent endpoint built with Instant API is below:

import OpenAI from 'openai';
const openai = new OpenAI(process.env.OPENAI_API_KEY);

/**
 * Streams results for our lovable assistant
 * @param {string} query The question for our assistant
 * @stream {object}   chunk
 * @stream {string}   chunk.id
 * @stream {string}   chunk.object
 * @stream {integer}  chunk.created
 * @stream {string}   chunk.model
 * @stream {object[]} chunk.choices
 * @stream {integer}  chunk.choices[].index
 * @stream {object}   chunk.choices[].delta
 * @stream {?string}  chunk.choices[].delta.role
 * @stream {?string}  chunk.choices[].delta.content
 * @returns {object} message
 * @returns {string} message.content
 */
export async function GET (query, context) {
  const completion = await openai.chat.completions.create({
    messages: [
      {role: `system`, content: `You are a lovable, cute assistant that uses too many emojis.`},
      {role: `user`, content: query}
    ],
    model: `gpt-3.5-turbo`,
    stream: true
  });
  const messages = [];
  for await (const chunk of completion) {
    // Stream our response as text/event-stream when ?_stream parameter added
    context.stream('chunk', chunk); // chunk has the schema provided above
    messages.push(chunk?.choices?.[0]?.delta?.content || '');
  }
  return {content: messages.join('')};
};

All @stream definitions follow the same Type Safety rules as @param and @returns directives.

To send a streaming response to the client, use context.stream(name, payload) where name is the name of the stream and payload adheres to the correct type definition for the stream.

Note: You must import context properly by adding it as the final parameter in your function arguments.

By default, API endpoints will not stream responses. You must activate a Server-Sent event by sending the _stream parameter. Provided the endpoint defined above, here is what you would receive given different URL accession patterns:

localhost:8000/assistant?query=how%20are%20you%20today?
{
  "content": "Hey there! 💁‍♀️ I'm doing great, thank you! 💖✨ How about you? 😊🌈"
}

Appending _stream to your query parameters gives you a live stream between a @begin and @response event, the latter returning an object.http payload containing the typical expected response for the event:

localhost:8000/assistant?query=how%20are%20you%20today?&_stream
id: 2023-10-25T04:29:59.115000000Z/2e7c7860-4a66-4824-98fa-a7cf71946f19
event: @begin
data: "2023-10-25T04:29:59.115Z"

[... more events ...]

event: chunk
data: {"id":"chatcmpl-8DPoluIgN4TDIuE1usFOKTLPiIUbQ","object":"chat.completion.chunk","created":1698208199,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" 💯"},"finish_reason":null}]}

[... more events ...]

event: @response
data: {"statusCode":200,"headers":{"X-Execution-Uuid":"2e7c7860-4a66-4824-98fa-a7cf71946f19","X-Instant-Api":"true","Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, OPTIONS, HEAD, PUT, DELETE","Access-Control-Allow-Headers":"","Access-Control-Expose-Headers":"x-execution-uuid, x-instant-api, access-control-allow-origin, access-control-allow-methods, access-control-allow-headers, x-execution-uuid","Content-Type":"application/json"},"body":"{\"content\":\"Hey there! 🌞 I'm feeling 💯 today! Full of energy and ready to help you out. How about you? How are you doing? 🌈😊\"}"}

If you do not want to listen to all streams, you can send in the _stream parameter as an object with keys-value pairs corresponding to the streams you want to listen for. Anything with a truthy value will be listened to, and * means "all streams".

e.g. localhost:8000/assistant/?_stream={"chunk":true} would only listen for a chunk stream.

When you are developing against external webhooks, especially chatbots, sometimes it is important to immediately return a 200 OK response to a third party server to acknowledge receipt of the message. Instant API comes packaged with a "magic" _background parameter which can enable background execution on specified endpoints: the endpoint will return 200 OK instantly and continue processing as normal behind the scenes.

To enable background processing for an endpoint, simply add a @background directive to the endpoint's JSDoc comment block:

/**
 * @background
 */
export async function GET (context) {

  const params = context.params; // Get all JSON params sent to endpoint

  // pseudocode
  await doSomethingThatTakesAwhile();
  await sendDiscordMessage();
  return {"complete": true};

}

Background-enabled functions won't execute as background functions by default, you have to supply them with the _background query or body parameter.

If you execute the above endpoint normally, like localhost:8000/my-webhook, you would get;

However, if you execute using localhost:8000/my-webhook?_background you see:

initiated "my-webhook#GET" ...

That means it's working as expected.

Multiple modes are supported for the @background directive: info, empty and params.

  • @background info is the default and equivalent to @background, it will always return initiated function_name ...
  • @background empty returns a completely empty body
  • @background params returns all parameters provided to the function
    • You can choose to return a subset of params by including them in a space-separated list
    • e.g. @background params name created_at would only reflect back {"name": "...", "created_at": "..."} if provided

Sometimes, and especially when dealing with LLMs, you find you have long-running endpoints that can be a pain to debug. Instant API exposes two methods, context.log() and context.error() that can be streamed to browser output as a Server-Sent Event even if streaming is not enabled on your endpoint. Debugging is only available when NODE_ENV=development.

Simply append _debug to your query parameters (or add it to your request body) to see context.log() and context.error() output in real-time.

These methods are effectively wrappers for context.stream('@stdout', payload) and context.stream('@stderr', payload) - only they don't require streams to be enabled to use. They'll output as streams with the event types @stdout and @stderr, respectively, when the _debug parameter is passed in.

Given the following endpoint:

const sleep = t => new Promise(res => setTimeout(() => res(), t));

export async function GET (context) {
  context.log(`Started!`);
  await sleep(100);
  context.error(`Oh no.`);
  await sleep(500);
  context.log(`OK!`);
  return {complete: true};
}

Calling it normally would result in:

localhost:8000/test-debug

But appending _debug in a development environment...

localhost:8000/test-debug?_debug
id: 2023-10-26T00:16:42.732000000Z/cae5a3c8-14df-4222-b762-fa3f16645fe7
event: @begin
data: "2023-10-26T00:16:42.732Z"

id: 2023-10-26T00:16:42.732000001Z/cae5a3c8-14df-4222-b762-fa3f16645fe7
event: @stdout
data: "Started!"

id: 2023-10-26T00:16:42.834000000Z/cae5a3c8-14df-4222-b762-fa3f16645fe7
event: @stderr
data: "Oh no."

id: 2023-10-26T00:16:43.337000000Z/cae5a3c8-14df-4222-b762-fa3f16645fe7
event: @stdout
data: "OK!"

event: @response
data: {"statusCode":200,"headers":{"X-Execution-Uuid":"cae5a3c8-14df-4222-b762-fa3f16645fe7","X-Debug":true,"X-Instant-Api":"true","Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, OPTIONS, HEAD, PUT, DELETE","Access-Control-Allow-Headers":"","Access-Control-Expose-Headers":"x-execution-uuid, x-debug, x-instant-api, access-control-allow-origin, access-control-allow-methods, access-control-allow-headers, x-execution-uuid","Content-Type":"application/json"},"body":"{\"complete\":true}"}

As you develop with Instant API, you may encounter various error types as you test the limits and boundariess of the framework. They are typically in the format:

{
  "error": {
    "type": "NamedError",
    "message": "error message",
    "stack": "",
    "details": {}
  }
}

Note that error.stack will not appear when NODE_ENV=production. The details object may or may not be present. A list of errors you may encounter are below:

Error Type Description Details Object
WellKnownError Error loading a schema from /.well-known/ pathname N/A
ClientError Generic 4xx, usually 400 (Bad Request) N/A
ServerError Generic 5xx, usually 500 (Internal Server Error) N/A
BadRequestError Returned when throw new Error('400: [...]') is called N/A
UnauthorizedError Returned when throw new Error('401: [...]') is called N/A
PaymentRequiredError Returned when throw new Error('402: [...]') is called N/A
ForbiddenError Returned when throw new Error('403: [...]') is called N/A
NotFoundError Returned when throw new Error('404: [...]') is called N/A
ParameterParseError Could not parse parameters based on content-type N/A
ParameterError @param validation failed {[field]: {message: 'string', invalid: true, mismatch: 'obj.a.b', expected: {type}, actual: {value, type}}}
ValueError @returns validation failed Same as ParameterError, only with "returns" as the field
StreamParameterError @stream validation failed Same as ParameterError, only with the stream name as the field
StreamError Stream specified with context.stream() does not exist N/A
StreamListenerError Specific stream name via _stream: {name: true} does not exist N/A
OriginError Invalid @origin specified N/A
DebugError Could not debug endpoint with _debug, usually permission issue N/A
ExecutionModeError Could not execute with _stream or _background because they are not enabled N/A
TimeoutError Endpoint took too long to execute. Configurable on gateway initialization. N/A
FatalError The gateway had a fatal crash event, status code 500 N/A
RuntimeError An error was thrown during endpoint execution, status code 420 N/A
InvalidResponseHeaderError A header specified in an object.http return statement was invalid Object containing invalid headers
AccessSourceError Placeholder: Currently unused, intended for platform gateways. N/A
AccessPermissionError Placeholder: Currently unused, intended for platform gateways. N/A
AccessAuthError Placeholder: Currently unused, intended for platform gateways. N/A
AccessSuspendedError Placeholder: Currently unused, intended for platform gateways. N/A
OwnerSuspendedError Placeholder: Currently unused, intended for platform gateways. N/A
OwnerPaymentRequiredError Placeholder: Currently unused, intended for platform gateways. N/A
RateLimitError Placeholder: Currently unused, intended for platform gateways. N/A
AuthRateLimitError Placeholder: Currently unused, intended for platform gateways. N/A
UnauthRateLimitError Placeholder: Currently unused, intended for platform gateways. N/A
SaveError Placeholder: Currently unused, intended for platform gateways. N/A
MaintenanceError Placeholder: Currently unused, intended for platform gateways. N/A
UpdateError Placeholder: Currently unused, intended for platform gateways. N/A
AutoformatError Placeholder: Currently unused, intended error if formatting on static resources goes awry. N/A

Instant API comes packaged with a TestEngine class for automated testing. It designed to work best with mocha and chai but can be used with any runtime.

The best way to get started with tests is via the instant.dev command line tools. Once your project is initialized, you can generate tests for your endpoints with:

instant g:test --function path/to/index.mjs

Where ./functions/path/to/index.mjs is an endpoint. This will automatically create a test for each exported function (HTTP method) on the endpoint in: ./test/tests/functions/path/to/index.mjs.

Additionally, if you are using Instant ORM, you can generate tests for individual models using:

instant g:test --model users

Which will create a test in ./test/tests/models/user.js.

Finally, To create blank (empty) tests use:

instant g:test path/to/my_test_name

Which will create an empty test in ./test/tests/path/to/my_test_name.mjs.

Tests can be run via:

If you want to write your own tests without using the instant.dev command line tools, you should first install mocha and chai.

npm i mocha --save-dev
npm i chai --save-dev

Next, create the file ./test/run.mjs with the following contents:

import InstantAPI from '@instant.dev/api';

const Gateway = InstantAPI.Gateway;
const TestEngine = InstantAPI.TestEngine;
const PORT = 7357; // Leetspeak for "TEST"; can be anything

// Load environment variables; make sure NODE_ENV is "test"
process.env.NODE_ENV = `test`;

// Initialize and load tests; set PORT for request mocking
const testEngine = new TestEngine(PORT);
await testEngine.initialize('./test/tests');

// Setup; create objects and infrastructure for tests
// Arguments returned here will be sent to .finish()
testEngine.setup(async () => {

  console.log();
  console.log(`# Starting test gateway on localhost:${PORT} ... `);
  console.log();

  // Start Gateway; {debug: true} will print logs
  const gateway = new Gateway({debug: false});
  gateway.load(process.cwd());       // load routes from filesystem
  gateway.listen(PORT);              // start server

  return { gateway };

});

// Run tests; use first argument to specify a test
const args = process.argv.slice(3);
if (args[0]) {
  await testEngine.run(args[0]);
} else {
  await testEngine.runAll();
}

// Finish; close Gateway and disconnect from database
// Receive arguments from .setup()
testEngine.finish(async ({ gateway }) => {
  gateway.close();
});

Finally, you can then add the following to package.json:

  "scripts": {
    "test": "mocha test/run.mjs"
  }

Tests can then be run via:

All tests should be put in the ./test/tests directory. This directory will be used by TestEngine to load all of your tests. Tests are imported when they are run, not when they are first initialized in TestEngine.

The structure of a test should look like this;

// Imports, setup to begin with
import chai from 'chai';
const expect = chai.expect;

// Optional: Name your test file
export const name = `Some tests`;

/**
 * Your tests
 * @param {any} setupResult Result of the function passed to `.setup()` in `test/run.mjs`
 */
export default async function (setupResult) {

  before(async () => {
    // any necessary setup
  });

  it('Should test for truthiness of "true"', async () => {

    expect(true).to.equal(true);

  });

  after(async () => {
    // any necessary teardown
  });

};

Tests are run top-down, depth-first, alphabetically. Given the following directory structure:

- test/
  - tests/
    - a/
      - second.mjs
      - a/third.mjs
      - b/fourth.mjs
    - b/
      - fifth.mjs
    - first.mjs
  • All tests in the root test/tests directory are executed first, alphabetically
  • All directories in test/tests are organized alphabetically
  • All tests in test/tests/a/ would then run
  • Then tests in test/tests/a/a/
  • Then tests in test/tests/a/b/
  • then tests in test/tests/b/

Giving a test order of:

- test/tests/first.mjs
- test/tests/a/second.mjs
- test/tests/a/a/third.mjs
- test/tests/a/b/fourth.mjs
- test/tests/b/fifth.mjs

If you set up via instant.dev command line tools, just run:

If you are running a custom installation, use:

Instant API can be deployed out-of-the-box to any host that respects the [package.json].scripts.start field. This includes AWS Elastic Beanstalk and Heroku. Vercel requires a little bit of finagling. However, the instant.dev command line tools will automatically manage deployments to both AWS Elastic Beanstalk and Vercel for you.

Simple;

instant deploy:config # follow instructions; choose Vercel or AWS
instant deploy --env staging # for Elastic Beanstalk
instant deploy --env preview # for Vercel
instant deploy --env production # Works for either

We defer to platform-specific Node.js deployment instructions. If you would like to contribute some helpful tips, please submit a PR on this README!

You can configure some custom options in your instant.mjs startup script.

import InstantAPI from '@instant.dev/api';

// ... other code ...

const Gateway = InstantAPI.Daemon.Gateway;

const gateway = new Gateway({
  port: 8000, //Defaults to 8170
  name: 'My API Server', // Defaults to "InstantAPI.Gateway"
  maxRequestSizeMB: 16, // Requests above this size will error. Defaults to 128MB.
  defaultTimeout: 10000 // Max execution time in ms. Defaults to 600000 (10 mins).
  debug: true // Whether to show logs or not, defaults to false
});

Currently logging is controlled entirely via the debug parameter in new Gateway() instatiation. When set to true this can very quickly overwhelm log storage on high-volume API servers. We're interested in getting feedback on your favorite log draining solutions and if there are elegant ways to integrate. Submit an issue to talk to us about it!

The Gateway instant comes with a built-in setErrorHandler() method to capture internal errors. You can add it to instant.mjs or your startup script easily. Here's an example using Sentry:

import InstantAPI from '@instant.dev/api';
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: '__DSN__',
  // ...
});

// ... other code ...

const Gateway = InstantAPI.Daemon.Gateway;

const gateway = new Gateway({port: process.env.PORT});
gateway.setErrorHandler(e => Sentry.captureException(e));
gateway.load(process.cwd());
gateway.listen();

This will catch all internal errors with the exception of;

  • Validation errors:
    • ExecutionModeError, ParameterParseError, ParameterError, StreamError, StreamParameterError
  • Intentionally thrown client errors:
    • BadRequestError, UnauthorizedError, PaymentRequiredError, ForbiddenError, NotFoundError

For the most part, Instant API has pretty comprehensive body parsing and parameter validation libraries built right in. However, you still might want to write or use custome middleware for repeated tasks; like authenticating users based on headers.

We recommend a simple approach of a middleware/ directory with files that each output a function that takes context as a single argument:

e.g. in ./middleware/authenticate.mjs

export default async (context) {
  let headers = context.https.headers;
  let result = await authenticateUser(headers);
  if (result) {
    return true;
  } else {
    throw new Error(`401: Unauthorized, failed header validation`);
  }
}

Then in your endpoints, you can do the following;

import authenticate from '../middleware/authenticate.mjs`;

export async function GET (context) => {
  await authenticate(context);
  return 'authenticated!';
}

We're just getting the first release of Instant API into the wild now, we're excited to hear feedback! It's a pretty comprehensive framework we've been developing for years and we're happy with the feature set, but please let us know if there are things we are missing by opening an issue.

Special thank you to Scott Gamble who helps run all of the front-of-house work for instant.dev 💜!



from Hacker News https://ift.tt/bWIUuAd

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.