aws-lite
is a simple, extremely fast, extensible AWS client for Node.js.(It's got good error reporting, too.)
aws-lite
is developed and maintained by the folks at OpenJS Foundation Architect. We <3 AWS!
Amazon has historically done a great job of maintaining its SDKs. However, its JavaScript SDKs are huge, with lots of generated code. This results in things like very slow instantiation (example: >400ms to load a single AWS client in SDK v3, and >500ms in v2), and reporting errors without usable stack traces.
We built aws-lite
to provide a simpler, faster, more stable position from which to work with AWS services in Node.js.
Install the client:
npm i @aws-lite/client
You can use the client as-is to quickly interact with AWS service APIs, or extend it with specific service plugins like so:
npm i @aws-lite/dynamodb
/**
* Instantiate a client
* This is a synchronous operation that will attempt to load your AWS credentials, local configuration, region settings, etc.
*/
import awsLite from '@aws-lite/client'
const config = { region: 'us-west-1' } // Optional
const aws = await awsLite(config)
/**
* Reads
* Fire a GET request by specifying an AWS service name and endpoint
*/
await aws({
service: 'lambda',
endpoint: '/2015-03-31/functions/my-lambda-name/configuration',
})
// {
// FunctionName: 'my-lambda-name',
// Runtime: 'nodejs18.x',
// ...
// }
/**
* Writes
* POST JSON to an endpoint by adding a payload property
*/
await aws({
service: 'lambda',
endpoint: '/2015-03-31/functions/my-lambda-name/invocations',
payload: { ok: true },
})
// ... whatever your Lambda returned
The following options may be passed when instantiating the aws-lite
client:
accessKeyId
(string)- AWS access key; if not provided, defaults to
AWS_ACCESS_KEY_ID
orAWS_ACCESS_KEY
env vars, and then to a~/.aws/credentials
file, if present - Manually specify a credentials file location with the
AWS_SHARED_CREDENTIALS_FILE
env var - If no access key is found,
aws-lite
will throw
- AWS access key; if not provided, defaults to
secretAccessKey
(string)- AWS secret key; if not provided, defaults to
AWS_SECRET_ACCESS_KEY
orAWS_SECRET_KEY
env vars, and then to a~/.aws/credentials
file, if present - Manually specify a credentials file location with the
AWS_SHARED_CREDENTIALS_FILE
env var - If no secret key is found,
aws-lite
will throw
- AWS secret key; if not provided, defaults to
sessionToken
(string)- AWS session token; if not provided, defaults to
AWS_SESSION_TOKEN
env var, and then to a~/.aws/credentials
file, if present - Manually specify a credentials file location with the
AWS_SHARED_CREDENTIALS_FILE
env var
- AWS session token; if not provided, defaults to
region
(string)- AWS service region (e.g.
us-west-1
); if not provided, defaults toAWS_REGION
,AWS_DEFAULT_REGION
, orAMAZON_REGION
env vars - By default, a
~/.aws/config
(or custom) file will only be loaded by making theAWS_SDK_LOAD_CONFIG
env var true - Manually specify a config file location with the
AWS_CONFIG_FILE
(andAWS_SDK_LOAD_CONFIG
) env var - If no region is found,
aws-lite
will throw - Region setting can be overridden per-request
- AWS service region (e.g.
profile
(string)- AWS config + credentials profile; if not provided, defaults to
AWS_PROFILE
env var, and then to thedefault
profile, if present
- AWS config + credentials profile; if not provided, defaults to
autoloadPlugins
(boolean) [default = true]- Automatically load installed
@aws-lite/*
+aws-lite-plugin-*
plugins
- Automatically load installed
debug
(boolean) [default = false]- Enable debug logging to console
endpointPrefix
(string)- Add prefix to endpoint requests, helpful for local testing
host
(string)- Set a custom host name to use, helpful for local testing
keepAlive
(boolean) [default = true]- Disable Node.js's connection keep-alive, helpful for local testing
plugins
(array)- Define
aws-lite
plugins to load; can be module names (e.g.@aws-lite/dynamodb
) or file paths on the local machine (e.g./path/to/my/plugin.mjs
) - By default, all installed official plugins (prefixed with
@aws-lite/
) and unofficial plugins (prefixed withaws-lite-plugin-
) will be loaded - Specifying plugins will disable auto-loading plugins
- Define
port
(number)- Set a custom port number to use, helpful for local testing
protocol
(string) [default =https
]- Set the connection protocol to
http
, helpful for local testing
- Set the connection protocol to
responseContentType
(string)- Set an overriding Content-Type header for all responses, helpful for local testing
An example:
import awsLite from '@aws-lite/client'
// Minimal: load everything from env vars
aws = await awsLite()
// Maximal: specify everything yourself
aws = await awsLite({
accessKeyId: '$accessKey',
secretAccessKey: '$secretKey',
sessionToken: '$sessionToken',
region: 'us-west-1',
profile: 'work',
autoloadPlugins: false, // Not necessary if manually specifying plugins
debug: true,
endpointPrefix: '/test/path/',
host: 'localhost',
keepAlive: false,
plugins: [ '@aws-lite/dynamodb', '/a/custom/local/plugin/path' ],
port: 12345,
protocol: 'http',
})
The following parameters may be passed with individual client requests; only service
is required:
awsjson
(boolean or array)- Enables AWS-flavored JSON encoding; if boolean, your entire body will be encoded; if an array, the key names specified in the array will be encoded, leaving other keys in normal JSON
- Do not use this option if you intend to pass your own pre-serialized AWS-flavored JSON in the
payload
endpoint
(string) [default =/
]- API endpoint your request will be made to
headers
(object)- Header names + values to be added to your request
- By default, all headers are included in authentication via AWS signature v4
- If your request includes a
payload
that cannot be automatically JSON-encoded and you do not specify acontent-type
header, the defaultapplication/octet-stream
will be used
payload
(object, buffer, readable stream, string)- Aliases:
body
,data
,json
- Payload to be used as the HTTP request body; as a convenience, any passed objects are automatically JSON-encoded (with the appropriate
content-type
header set, if not already present); buffers, streams, and strings simply pass through as-is - Readable streams are currently experimental
- Passing a Node.js readable stream initiates an HTTP data stream to the API endpoint instead of writing a normal HTTP body
- Streams are not automatically signed like normal HTTP bodies, and may require their own signing procedures, as in S3
- Aliases:
paginate
(boolean) [experimental]- Enables (or disables) automatic result pagination; if pagination is enabled by default (see
paginator.default
), passfalse
to disable automatic pagination
- Enables (or disables) automatic result pagination; if pagination is enabled by default (see
paginator
(object) [experimental]- Enable automatic pagination for service API via the following properties:
type
(string) [default =payload
] -payload
(default) passes the cursor via body payload,query
passes the cursor via query string parametercursor
(string) [required] - pagination token from the prior response payload to be passed with the current request; example: in S3 this is the query string parametercontinuation token
token
(string) [required] - pagination token returned in each response payload to be passed with the next request ascursor
; example: in S3, this would beNextContinuationToken
accumulator
(string) [required] - Response payload array property name to aggregate into final result set; example: in S3, this would beContents
default
(string) - set value toenabled
to enable pagination for all applicable requests by default; individual requests can opt out of pagination by settingpaginate
tofalse
- Enable automatic pagination for service API via the following properties:
query
(object)- Serialize the passed object and append it to your
endpoint
as a query string in your request
- Serialize the passed object and append it to your
service
(string) [required]- AWS service code, usually just the lowercase form of the service name (e.g.
DynamoDB
=dynamodb
); full list can be found here
- AWS service code, usually just the lowercase form of the service name (e.g.
Additionally, the following configuration options can be specified in each request, overriding those specified by the instantiated client:
region
,protocol
,host
, andport
An example:
import awsLite from '@aws-lite/client'
const aws = await awsLite()
// Make a plain JSON request
await awsLite({
service: 'lambda',
endpoint: '/2015-03-31/functions/$function-name/invocations',
query: { Qualifier: '1' }, // Invoke's version / alias '1'
payload: { ok: true }, // Object will be automatically JSON-encoded
})
// Make an AWS-flavored JSON request
await awsLite({
service: 'dynamodb',
headers: { 'X-Amz-Target': `DynamoDB_20120810.GetItem` },
awsjson: [ 'Key' ], // Ensures only payload.Key will become AWS-flavored JSON
payload: {
TableName: '$table-name',
Key: { myHashKey: 'Gaal', mySortKey: 'Dornick' }
},
})
// Paginate results
await awsLite({
service: 'dynamodb',
headers: { 'X-Amz-Target': `DynamoDB_20120810.Scan` },
paginator: {
cursor: 'ExclusiveStartKey',
token: 'LastEvaluatedKey',
accumulator: 'Items',
enabled: 'default',
},
payload: {
TableName: '$table-name',
},
})
The following properties are returned with each non-error client response:
statusCode
(number)- HTTP status code of the response
headers
(object)- Response header names + values
payload
(object, string, null)- Response payload; as a convenience, JSON-encoded responses are automatically parsed; XML-encoded responses are returned as plain strings
- Responses without an HTTP body return a
null
payload
An example:
import awsLite from '@aws-lite/client'
const aws = await awsLite()
await awsLite({
service: 'lambda',
endpoint: '/2015-03-31/functions/$function-name/configuration',
})
// {
// statusCode: 200,
// headers: {
// 'content-type': 'application/json',
// 'x-amzn-requestid': 'ba3a55d2-16c2-4c2b-afe1-cf0c5523040b',
// ...
// },
// payload: {
// FunctionName: '$function-name',
// FunctionArn: 'arn:aws:lambda:us-west-1:1234567890:function:$function-name',
// Role: 'arn:aws:iam::1234567890:role/$function-name-role',
// Runtime: 'nodejs18.x',
// ...
// }
// }
Out of the box, @aws-lite/client
is a full-featured AWS API client that you can use to interact with any AWS service that makes use of authentication via AWS signature v4 (which should be just about all of them).
@aws-lite/client
can be extended with plugins to more easily interact with AWS services, or provide custom behavior or semantics. As such, plugins enable you to have significantly more control over the entire API request/response lifecycle.
A bit more about how plugins work:
- Plugins can be authored in ESM or CJS
- Plugins can be dependencies downloaded from npm, or also live locally in your codebase
- In conjunction with the open source community,
aws-lite
publishes service plugins under the@aws-lite/$service
namespace that conform toaws-lite
standards @aws-lite/*
plugins, and packages published to npm with theaws-lite-plugin-*
prefix, are automatically loaded by the@aws-lite/client
upon instantiation- This behavior can be overridden with the
autoloadPlugins
parameter
- This behavior can be overridden with the
Thus, to make use of the @aws-lite/dynamodb
plugin, this is what your code would look like:
npm i @aws-lite/client @aws-lite/dynamodb
import awsLite from '@aws-lite/client'
const aws = await awsLite() // @aws-lite/dynamodb is now loaded
aws.dynamodb.PutItem({ TableName: 'my-table', Key: { id: 'hello' } })
The aws-lite
plugin API is lightweight and simple to learn. It makes use of four optional lifecycle hooks:
validate
[optional] - an object of property names and types used to validate inputs pre-requestrequest()
[optional] - an async function that enables mutation of inputs to the final service API requestresponse()
[optional] - an async function that enables mutation of service API responses before they are returnederror()
[optional] - an async function that enables mutation of service API errors before they are returned
The above four lifecycle hooks must be exported as an object named methods
, along with a valid AWS service code property named service
, like so:
// A simple plugin for validating `TableName` input on dynamodb.PutItem() calls
export default {
service: 'dynamodb',
awsDoc: 'https://docs.aws.../API_PutItem.html',
readme: 'https://github...#PutItem',
methods: {
PutItem: {
validate: {
TableName: { type: 'string', required: true }
}
}
}
}
// Using the above plugin
aws.dynamodb.PutItem({ TableName: 12345 }) // Throws validation error
Additionally, two optional (but highly recommended) metadata properties that will be included in any method errors:
awsDoc
(string) [optional] - intended to be a link to the AWS API doc pertaining to this method; should usually start withhttps://docs.aws.amazon.com/...
readme
(string) [optional] - a link to a relevant section in your plugin's readme or docs
Example plugins can be found below, in plugins/
dir (containing @aws-lite/*
plugins), and in tests.
The validate
lifecycle hook is an optional object containing (case-sensitive) input property names, with a corresponding object that denotes their type
and whether required
.
Types are as follows: array
boolean
number
object
string
. An example validate
plugin:
// Validate inputs for a single DynamoDB method (`CreateTable`)
export default {
service: 'dynamodb',
methods: {
CreateTable: {
validate: {
TableName: { type: 'string', required: true },
AttributeDefinitions: { type: 'array', required: true },
KeySchema: { type: 'array', required: true },
BillingMode: { type: 'string' },
DeletionProtectionEnabled: { type: 'boolean' },
GlobalSecondaryIndexes: { type: 'array' },
LocalSecondaryIndexes: { type: 'array' },
ProvisionedThroughput: { type: 'object' },
SSESpecification: { type: 'object' },
StreamSpecification: { type: 'object' },
TableClass: { type: 'string' },
Tags: { type: 'array' },
}
}
}
}
The request()
lifecycle hook is an optional async function that enables that enables mutation of inputs to the final service API request.
request()
is executed with two positional arguments:
params
(object)- The method's input parameters
utils
(object)
The request()
method may return nothing, or a valid client request. An example:
// Automatically serialize input to AWS-flavored JSON
export default {
service: 'dynamodb',
methods: {
PutItem: {
validate: { Item: { type: 'object', required: true } },
request: async (params, utils) => {
params.Item = utils.awsjsonMarshall(params.Item)
return {
headers: { 'X-Amz-Target': `DynamoDB_20120810.PutItem` }
payload: params
}
}
}
}
}
The response()
lifecycle hook is an async function that enables mutation of service API responses before they are returned.
response()
is executed with two positional arguments:
response
(object)- An object containing three properties from the API response:
statusCode
(number)- HTTP response status code
headers
(object)- HTTP response headers
payload
(object or string)- Raw non-error response from AWS service API request; if the entire payload is JSON or AWS-flavored JSON,
aws-lite
will attempt to parse it prior to executingresponse()
. Responses that are primarily JSON, but with nested AWS-flavored JSON, will be parsed only as JSON and may require additional deserialization with theawsjsonUnmarshall
utility orawsjson
property
- Raw non-error response from AWS service API request; if the entire payload is JSON or AWS-flavored JSON,
- An object containing three properties from the API response:
utils
(object)
The response()
method may return: nothing (which will pass through the response
object as-is) or any data (most commonly an object or string, or mutated version of the response
object).
Should you return an object, you may also include an awsjson
property (that behaves the same as in client requests). The awsjson
property is considered reserved, and will be stripped from any returned data.
An example:
// Automatically deserialize AWS-flavored JSON
export default {
service: 'dynamodb',
methods: {
GetItem: {
// Assume successful responses always have an AWS-flavored JSON `Item` property
response: async (response, utils) => {
response.awsjson = [ 'Item' ]
return response // Returns the response (`statusCode`, `headers`, `payload`), with `payload.Item` unformatted from AWS-flavored JSON, and the `awsjson` property removed
}
}
}
}
The error()
lifecycle hook is an async function that enables mutation of service API errors before they are returned.
error()
is executed with two positional arguments:
error
(object)- The object containing the following properties:
error
(object or string): the raw error from the service API; if the entire error payload is JSON,aws-lite
will attempt to parse it into theerror
propertymetadata
(object) -aws-lite
error metadata; to improve the quality of the errors presented byaws-lite
, please only append to this objectstatusCode
(number or undefined) - resulting status code of the API response; if an HTTP connection error occurred, nostatusCode
will be present
- The object containing the following properties:
utils
(object)
The error()
method may return nothing, a new or mutated version of the error payload it was passed, a string, an object, or a JS error. An example:
// Improve clarity of error output
export default {
service: 'lambda',
methods: {
GetFunctionConfiguration: {
error: async (err, utils) => {
if (err.statusCode === 400 &&
err?.error?.message?.match(/validation/)) {
// Append a property to be clearly displayed along with the other error data
err.metadata.type = 'Validation error'
}
return err
}
}
}
}
request()
, response()
, and error()
are all passed a second argument of helper utilities and data pertaining to the client:
awsjsonMarshall
(function)- Utility for marshalling data to the format underlying AWS-flavored JSON serialization; accepts a plain object, returns a marshalled object
awsjsonUnmarshall
(function)- Utility for unmarshalling data from the format underlying AWS-flavored JSON serialization; accepts a marshalled object, returns a plain object
config
(object)- The current client configuration; any configured credentials are found in the
credentials
object
- The current client configuration; any configured credentials are found in the
credentials
(object)accessKeyId
,secretAccessKey
, andsessionToken
being used in this request- Note:
secretAccessKey
andsessionToken
are present in this object, but non-enumerable
region
(string)- Canonical service region being used in this request; this value may differ from the region set in the
config
object if overridden per-request
- Canonical service region being used in this request; this value may differ from the region set in the
An example of plugin utils:
async function request (params, utils) {
let awsStyle = utils.awsjsonMarshall({ ok: true, hi: 'there' })
console.log(marshalled) // { ok: { BOOL: true }, hi: { S: 'there' } }
let plain = utils.awsjsonUnmarshall({ ok: { BOOL: true }, hi: { S: 'there' } })
console.log(unmarshalled) // { ok: true, hi: 'there' }
console.log(config) // { profile: 'my-profile', autoloadPlugins: true, ... }
console.log(credentials) // { accessKeyId: 'abc123...' } secrets are non-enumerable
console.log(region) // 'us-west-1'
}
AWS has (as of this writing) nearly 300 service APIs – aws-lite
would love your help in authoring and maintaining official (and unofficial) plugins!
- Pull down this repo
- Install dependencies and run the normal test suite:
npm it
- To create an
@aws-lite/*
plugin:- Add your plugin to the
plugins
file - Run
npm run gen
- Add your plugin to the
- Create a PR that adheres to our testing methodology
It is advisable you have AWS credentials on your local development machine for manual verification of any client or service plugin changes
Similar to the Definitely Typed (@types
) model, aws-lite
releases packages maintained by third parties under the @aws-lite/*
namespace.
Plugins released within the @aws-lite/*
namespace are expected to conform to the following standards:
@aws-lite/*
plugins should read more or less like the others, and broadly adhere to the following style:- Plugins should be authored in ESM, be functional (read: no classes), and avoid globals / closures, etc. wherever possible
- Plugins should be authored in JavaScript; those that require transpilation (e.g. TypeScript) will not be accepted
- Plugins should cover all documented methods for a given service, and include links for each method within the plugin
- Each plugin is singular for a given service
- Example: we will not ship
@aws-lite/lambda
,@aws-lite/lambda-1
,@aws-lite/lambda-new
, etc. - With permission of the current maintainer(s), you may become a maintainer of an existing plugin
- Example: we will not ship
- To maintain the speed, security, and lightweight size of the
aws-lite
project, plugins should ideally have zero external dependencies- If external dependencies are absolutely necessary, they should be justifiable; expect their inclusion to be heavily audited
- Ideally (but not necessarily), each plugin should include its own tests
- Tests should follow the project's testing methodology, utilizing
tape
as the runner andtap-arc
as the output parser - Tests should not rely on interfacing with live AWS services
- Tests should follow the project's testing methodology, utilizing
- Wherever possible, plugin maintainers should attempt to employ manual verification of their plugins during development
- By opting to author a plugin, you are opting to provide reasonably prompt bug fixes, updates, etc. for the community
- If you are not willing to make that kind of commitment but still want to publish your plugins publicly, please feel free to do so outside this repo with an
aws-lite-plugin-
package prefix
- If you are not willing to make that kind of commitment but still want to publish your plugins publicly, please feel free to do so outside this repo with an
Due to the mission-critical nature of this project, we strive for 100% test coverage on the core client. (We also acknowledge that 100% coverage does not mean 0 bugs, so meaningful and thorough tests are much appreciated.)
Due to the nature of interacting with AWS services, manual validation is not only often necessary, but in many cases it's required. (For example: running automated test suites on EKS may be slow, onerous, and financially expensive.)
One should expect that running the live AWS client test suite (npm run test:live
) will result in a limited number of free tier resources to be created in the account corresponding to your supplied (or default) AWS credentials. These resources should never exceed the free tier under normal circumstances.
To release an update to a @aws-lite/*
plugin, use the npm run plugin
script with similar syntax to the npm version
command, like so:
- Commit all your changes and start in a clean state
- Release a plugin (and its types):
npm run plugin [major|minor|patch] $plugin
- Example:
npm run plugin minor dynamodb
- Example:
- Patch a plugin's types only:
npm run plugin patch $plugin-types
- Example:
npm run plugin patch dynamodb-types
- Note: type changes can only be issued as patches
- Example:
- Push the commit