Scientyfic World

How to Build a Serverless Function That Calls ApyHub APIs (Node.js and AWS Lambda)

ApyHub is a catalogue of ready-made APIs (document conversion, validation, data lookups and more) that you call with one token. A serverless function is a small piece of code that...

Share:

Get an AI summary of this article

building Serverless Function using Apyhub banner image

ApyHub is a catalogue of ready-made APIs (document conversion, validation, data lookups and more) that you call with one token. A serverless function is a small piece of code that a cloud provider runs for you on demand. Put the two together and you can add features like “validate this VAT number” or “convert this file” to your app without writing or hosting those features yourself. This guide builds exactly that: a Node.js function that calls an ApyHub API, tested locally and deployed to AWS Lambda.

Updated October 2026: I rewrote this 2024 post because its central claim was wrong. The original said you deploy your function “to ApyHub”. ApyHub does not host serverless functions; per its own site it is an API catalogue, so your function has to run somewhere else (AWS Lambda here). I also replaced the old SDK-in-Next.js setup, since the apyhub npm package has not had a release since February 2023, with a plain fetch call to the REST API, and I updated the Lambda runtime and deployment commands. Free-plan limits and endpoints change, so check ApyHub’s pricing and the API page before you rely on them.

What You Are Building

The function takes a two-letter country code in a query string, asks ApyHub for details about that country, and returns a trimmed-down JSON response. It is a deliberately small example, but it shows every part of the pattern: an API token kept out of your code, input validation, a timeout on the outbound call, sensible error codes, and a local test that never touches the network.

PieceRole
ApyHubThe third-party API your function calls (authenticated with an apy-token header)
AWS LambdaRuns your function on demand; you pay per request and duration
Environment variableHolds the ApyHub token so it is not committed to your code
Local testChecks the handler with a fake fetch, so you spend no ApyHub quota while developing

Prerequisites

  • Node.js 22 or newer locally (Lambda supports the Node.js 22 and 24 runtimes, per the Lambda runtimes page, and both include fetch built in).
  • An ApyHub account and API token. Sign up at apyhub.com and create a token in your workspace settings. Its free plan is enough to try this.
  • An AWS account and the AWS CLI, configured with credentials that can create IAM roles and Lambda functions.

Step 1: Learn the ApyHub API

ApyHub APIs are plain HTTPS endpoints. You send your token in an apy-token header. As an example, the country information API is a GET request that takes a country query parameter (an ISO 3166-1 alpha-2 code such as DE) and returns a JSON object with a data field containing the country name (value), its code (key), the currency and the calling codes. Check it with curl before writing any code:

curl -H "apy-token: $APY_TOKEN" \
  "https://api.apyhub.com/apyhub/detailed-info-for-a-single-country?country=DE"

ApyHub’s pages show both api.apyhub.com and a regional api.eu.apyhub.com host. Use whichever your workspace and the API page tell you to; the handler below keeps the host in one constant so it is easy to change.

There is an official Node.js SDK, but its last release (0.1.12) was in February 2023, so for new code I prefer calling the REST API directly. It is one fetch call, with no dependency to keep patched.

Step 2: Write the Function

Create a folder with a file called index.mjs (the .mjs extension makes Node treat it as an ES module, so top-level export and fetch work without a build step):

// Lambda handler (ES module, Node.js 22+): look up country details through ApyHub.
const ENDPOINT = 'https://api.apyhub.com/apyhub/detailed-info-for-a-single-country';

const json = (statusCode, body) => ({
  statusCode,
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(body),
});

export const handler = async (event) => {
  const token = process.env.APY_TOKEN;
  if (!token) return json(500, { error: 'APY_TOKEN is not configured' });

  const country = String(event?.queryStringParameters?.country ?? '').toUpperCase();
  if (!/^[A-Z]{2}$/.test(country)) {
    return json(400, { error: 'Pass ?country= with a two-letter ISO 3166-1 code, e.g. DE' });
  }

  let res;
  try {
    res = await fetch(`${ENDPOINT}?country=${country}`, {
      headers: { 'apy-token': token },
      signal: AbortSignal.timeout(5000),
    });
  } catch (err) {
    console.error('ApyHub request failed', err.name);
    return json(504, { error: 'Upstream API did not respond in time' });
  }

  if (!res.ok) {
    console.error('ApyHub returned', res.status);
    return json(res.status === 404 ? 404 : 502, { error: `Upstream returned ${res.status}` });
  }

  const { data } = await res.json();
  return json(200, {
    country: data.value,
    code: data.key,
    currency: data.currency?.key,
    callingCodes: data.calling_codes,
  });
};

What this does, and why:

  • The token comes from process.env.APY_TOKEN, never from source code. If it is missing you get a clear 500 instead of a confusing upstream error.
  • Input is validated before the outbound call. Only a two-letter code is accepted, so a caller cannot push arbitrary text into the upstream URL or waste your ApyHub quota on junk.
  • The outbound call has a 5-second timeout (AbortSignal.timeout). Without one, a slow upstream keeps your function running and billing until Lambda’s own timeout.
  • Upstream errors map to honest status codes: 404 stays 404, a timeout becomes 504, and anything else becomes 502. The log line records the status but never the token.
  • The response is trimmed to the fields your app needs, which keeps your API stable even if the provider adds fields.

The handler’s input shape (event.queryStringParameters) matches what a Lambda function URL or API Gateway HTTP API sends, so the same code works behind either.

Step 3: Test It Locally Without Spending Quota

Replace fetch with a fake and call the handler directly. This test covers the success path, bad input, a missing token, an upstream 404 and an upstream timeout. Save it as test.mjs next to index.mjs (change the import to ./index.mjs):

import assert from 'node:assert/strict';
process.env.APY_TOKEN = 'test-token';
const calls = [];
globalThis.fetch = async (url, opts) => {
  calls.push([url, opts.headers]);
  if (url.endsWith('=XX')) return new Response('{}', { status: 404 });
  if (url.endsWith('=ZZ')) throw Object.assign(new Error('t'), { name: 'TimeoutError' });
  return new Response(JSON.stringify({ data: { key: 'DE', value: 'Germany', currency: { key: 'EUR' }, calling_codes: ['+49'] } }), { status: 200 });
};
const { handler } = await import('./index.mjs');
let r = await handler({ queryStringParameters: { country: 'de' } });
assert.equal(r.statusCode, 200); assert.deepEqual(JSON.parse(r.body), { country: 'Germany', code: 'DE', currency: 'EUR', callingCodes: ['+49'] });
assert.equal(calls[0][0], 'https://api.apyhub.com/apyhub/detailed-info-for-a-single-country?country=DE'); assert.equal(calls[0][1]['apy-token'], 'test-token');
assert.equal((await handler({ queryStringParameters: { country: 'Germany' } })).statusCode, 400);
assert.equal((await handler({})).statusCode, 400);
assert.equal((await handler({ queryStringParameters: { country: 'XX' } })).statusCode, 404);
assert.equal((await handler({ queryStringParameters: { country: 'ZZ' } })).statusCode, 504);
delete process.env.APY_TOKEN; assert.equal((await handler({ queryStringParameters: { country: 'DE' } })).statusCode, 500);
console.log('all handler tests passed');
node test.mjs
# all handler tests passed

I ran this exact test on Node.js while writing this post, and it passes. It proves the logic and the HTTP request shape (URL and header) but not that ApyHub’s live response still has the same fields, so do one real curl call (Step 1) with your own token as well.

Step 4: Deploy to AWS Lambda

Create the execution role

aws iam create-role --role-name country-lookup-role \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }]
  }'

aws iam attach-role-policy --role-name country-lookup-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

The managed policy lets the function write its logs to CloudWatch and nothing else. Wait a few seconds for the role to propagate before the next command.

Package and create the function

zip function.zip index.mjs

aws lambda create-function \
  --function-name country-lookup \
  --runtime nodejs22.x \
  --handler index.handler \
  --role arn:aws:iam::<ACCOUNT_ID>:role/country-lookup-role \
  --zip-file fileb://function.zip \
  --timeout 10 \
  --memory-size 128 \
  --environment "Variables={APY_TOKEN=$APY_TOKEN}"

Replace <ACCOUNT_ID> with your 12-digit account ID. The timeout of 10 seconds is deliberately longer than the 5-second upstream timeout in the code, so your handler can return its own 504 before Lambda kills it. I checked these request parameters against the current boto3 service model for CreateFunction, UpdateFunctionCode and Invoke, and both nodejs22.x and nodejs24.x are valid runtime values; I did not run them against a live AWS account.

About the token: environment variables are encrypted at rest but visible to anyone who can read the function’s configuration. For a hobby project that is acceptable. For production, store the token in AWS Secrets Manager or SSM Parameter Store and read it at start-up, and give the role permission to read only that one secret.

Invoke it

aws lambda invoke --function-name country-lookup \
  --cli-binary-format raw-in-base64-out \
  --payload '{"queryStringParameters":{"country":"DE"}}' \
  response.json

cat response.json

You should see a statusCode of 200 and a body with the country name, code, currency and calling codes. Logs are in CloudWatch under /aws/lambda/country-lookup. To ship a change, zip again and run aws lambda update-function-code --function-name country-lookup --zip-file fileb://function.zip.

Step 5: Expose It Over HTTP (Carefully)

To call the function from a browser or another service, give it a Lambda function URL or put an API Gateway HTTP API in front of it. Be careful with public access here: a public function that spends your ApyHub quota is an invitation to abuse. Per the Lambda documentation on function URL access, a function URL with auth type AWS_IAM only accepts requests signed by principals you allow, while auth type NONE lets anyone with the URL invoke it; for public URLs created with the CLI you must also add the resource-based permissions yourself, and since October 2025 new function URLs need both lambda:InvokeFunctionUrl and lambda:InvokeFunction.

If the function has to be public, add limits before you share the URL: throttle it (API Gateway usage plans or a reserved-concurrency cap on the function), keep the 2-letter validation, and set a billing alert. If it is only called by your own backend, keep AWS_IAM.

Securing and Operating the Function

  • Least privilege. The role above can only write logs. Add permissions only when the function needs them.
  • Never log the token or full upstream responses that may contain user data.
  • Rotate the token if it ever appears in logs, a commit or a screenshot, and update the environment variable afterwards.
  • Watch quota. ApyHub’s plans are measured in monthly usage allowances; cache repeated lookups (country data rarely changes) so the same request does not cost you twice.
  • Alarms. Create a CloudWatch alarm on the function’s Errors metric so failures reach you before your users.
  • Cost. Lambda bills per request and per GB-second of duration (see Lambda pricing); a function this small is cheap, and idle time costs nothing. Background on those trade-offs is in our serverless architecture guide.

Where to Go Next

Swap the endpoint for any other ApyHub API (file conversion, validation, text extraction) and keep the same structure: validate input, call with a timeout, map errors, return a trimmed result. If you need a full application rather than a single function, see how to choose a service and deploy a web app on AWS.

Can I deploy a serverless function to ApyHub?

No. ApyHub is an API catalogue that you call from your own code. You write the function and run it on a platform such as AWS Lambda, Google Cloud Functions, Azure Functions or Vercel, and that function calls ApyHub.

Do I need the ApyHub npm SDK?

No. The SDK exists but its last release was in February 2023. Every ApyHub endpoint is a normal HTTPS API authenticated with an apy-token header, so a plain fetch call is simple and has no dependency to maintain.

How do I keep my ApyHub token safe?

Never put it in source code. Store it in an environment variable at minimum, and for production use AWS Secrets Manager or SSM Parameter Store. Rotate it if it leaks.

Which Node.js version should I use on Lambda?

Use a currently supported runtime such as nodejs22.x or nodejs24.x. Both ship with fetch built in, so you do not need an HTTP library for this example.

Is it safe to make the function URL public?

Only with limits. A public function can be called by anyone, which can use up your ApyHub quota and your AWS budget. Prefer AWS_IAM auth for internal use, and add throttling and billing alerts if it must be public.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts