wappi
v2.0.13
Published
wappi microservice API framework
Downloads
573
Readme
WAPPI - Serverless API Framework
What is wappi?
wappi
is a framework that allows a functional API to be run serverless or as a monolith. It is designed to make creating an API backend, simple and organized. So even junior developers can pick it up in no time.
It incorporates testing, documentation, and simple local development in it's core. This way your code will be clean, documented and tested as a standard. wappi
will run locally with ease. It has templates for each endpoint out of the box. It incorporates prisma into it's dataLayer by default.wappi
is written in TypeScript, with types also incorporated in it's core.
wappi
pairs with wappi/frontend
which will link your frontend requests to your backend API in a simple structure. It's all JSON, so you don't have to learn a new syntax or language.
Install globally
Wappi is a npx package. To use without typing npx each time, install it globally:
npm install -g wappi
Or Use
npx wappi [command]
How to use:
When installed globally:
Commands
---------------------------------------------------------------------------------------------------------------
Command | Shorthand | Short Description | Example
--------|-----------|-----------------------------------------------------------------------|-------------------
create | c | Creates a new functional endpoint | wappi c
--------|-----------|-----------------------------------------------------------------------|-------------------
build | b | Creates the build for running locally | wappi b
--------|-----------|-----------------------------------------------------------------------|-------------------
delete | d | Deletes a current functional endpoint version and namespace | wappi d
--------|-----------|-----------------------------------------------------------------------|-------------------
help | h | Show this help menu | wappi h
--------|-----------|-----------------------------------------------------------------------|-------------------
init | i | Initializes WAPPI (required for all the other commands to work) | wappi i
--------|-----------|-----------------------------------------------------------------------|-------------------
lint | l | Runs eslint over code. Based off the .eslintrc.json | wappi l
--------|-----------|-----------------------------------------------------------------------|-------------------
run | r | Run wappi locally. Runs on the port set in config (wappi.json) | wappi r
--------|-----------|-----------------------------------------------------------------------|-------------------
test | t | Test all your code, using Jest | wappi t
--------|-----------|-----------------------------------------------------------------------|-------------------
update | u | update all your node modules, to be using the latest versions | wappi u
--------|-----------|-----------------------------------------------------------------------|-------------------
watch | w | Watches the API folder for changes, works with `wappi run` | wappi w
---------------------------------------------------------------------------------------------------------------
Note: If not installed globally, use it with npx. npx wappi [command]
Folder Structure
NameSpacing and best practices
- Always use camelCase, for all variable names, database tables names, etc.
wappi
returns all data with keys of objects as camelCase by default.
Endpoint (Function) Names
Each endpoint is PascalCase with a underscore _.
The underscore _
is to separate the namespace with the function purpose as well as the version number.
Example:
User_GetAllUsers_1_0_0
|| User_Update_2_0_2
|| CompanyProducts_AddAllBrandedProducts_2_0_2
|| Core_Login_1_1_0
The endpoints are grouped by the folder names, which are separated by the underscores.
So you can have many User
endpoints, and many User_Update
Versions. All easily organized.
DataLayer
The DataLayer folder is where you organize all your requests to databases and stores. This way you can reuse the same database calls over multiple endpoints.
Core
These are a few Core Files which are included by default. They help wappi
do auto authentication. Allows you to decode the JWT and retrieve infor from it relating to the user, etc.
CoreHelpers
These are simple helpers that allow you as a user to do some basic things.
If you want add your own helpers, add them here.
Logging
Logs in development is important.
wappi has logs by default in every endpoint.
You can customize how to treat your logs and where they should go in the CoreHelpers/logging.ts
file.
Note: When you run wappi update
, the Core & CoreHelpers folders will be updated with the latest versions of the default functions (Except Core_Login). So if you make changes to these default functions, they will be overridden. However, your custom functions will not be changed or removed.
Versioning
wappi
comes with versioning out of the box.
When running wappi create
you will have the option of copying the last versions code or creating a new version from scratch.
Why version?
When deploying a backend fix with a frontend, there is a fundamental issue if they have a dependency on each other. That is: you will need to deploy them at the same time to avoid any production issues.
With wappi
you will can create a new version of your endpoint. This will mean you can deploy this version to production first. It can be there because nothing will be using it. Then you deploy your front end code with the version update and other updates. Resulting in ZERO downtime and a happy CTO! ;)
Pre-requisite knowledge to make awesome APIs
- Basic Typescript
- Basic Jest (not hard to learn)
- JSON
- Or use Chat GPT...
How to Create a new endpoint?
To Create a new endpoints: run wappi c
You will be prompted to add in a namespace and function name.
Everything is always camelCase.
Except for the function name itself. This is PascalCase. Just as a Class would be named.
The function name also has underscores _ for namespaces. (Ex: User_GetAllUserData)
This is so all the functions can be grouped together.
When wappi c
is created it will create the following files and automatically name everything correctly.
NameSpace FunctionName Version (1.0.0) _data failData.ts // Data for the tests to run correctly successData.ts // Data for the tests to run correctly helpers Add Any Files you want in here that assist with your main function. This keeps the file structure tidy. index.ts // This is the main function file index.spec.ts // This is the test file that uses Jest. README.md // Where you document the endpoint purpose types.ts // Where you add all the types
File Structures
index.ts
This is the file that will be called through the wappi server
import { DB } from '../../../CoreHelpers/types'
import { createLog } from '../../../CoreHelpers/logging'
import { standardErrorLog, standardInitialLog } from '../../../CoreHelpers/logging'
import { Response, Body } from './types'
export async function NameSpace_FunctionName_1_0_0(
body: Body,
{ prisma }: DB,
): Promise<Response> {
try {
if (!body) throw new Error('No Body')
standardInitialLog(body, 'NameSpace_FunctionName_1_0_0')
// CODE GOES HERE
return {
success: true,
}
// eslint-disable-next-line no-unreachable
} catch (err) {
standardErrorLog(body, err, 'NameSpace_FunctionName_1_0_0')
return {
success: false,
error: 'Fatal error in NameSpace_FunctionName_1_0_0. Investigate.',
message: err.message,
}
}
}
index.spec.ts
import { db, disconnectPrisma } from '../../../CoreHelpers/JestPrisma'
import { NameSpace_FunctionName_1_0_0 } from './index'
import { successResponse, successBody } from './_data/successData'
import { failBody, failResponse } from './_data/failData'
afterAll(async () => { disconnectPrisma() })
it('Should be successful - NameSpace_FunctionName_1_0_0 Basic Test', async () => {
const { success } = await NameSpace_FunctionName_1_0_0(successBody, db)
expect(success).toBeTruthy()
})
it('Should be successful - NameSpace_FunctionName_1_0_0 Basic Test', async () => {
const response = await NameSpace_FunctionName_1_0_0(successBody)
expect(response).toEqual(successResponse)
})
it('Should fail - NameSpace_FunctionName_1_0_0 Basic Test', async () => {
// @ts-expect-error
const test = await NameSpace_FunctionName_1_0_0(failBody, db)
expect(test.success).toBeFalsy()
expect(test).toEqual(failResponse)
})
types.ts
This is the types file for the endpoint.
import { BaseAuthenticatedBody } from '../../../CoreHelpers/types'
export interface EndPointBody {
// Add any types you want the endpoint to validate here
// These types are passed through in the body.
}
export interface SuccessResponse {
success: true
// Any items in the response object need to be typed here.
}
export interface ErrorResponse {
success: false
error: string
message: string
}
export type Response = SuccessResponse | ErrorResponse
export interface Body extends EndPointBody, BaseAuthenticatedBody {}
failData.ts
This file is for all the Jest test fail data to be passed into the body of the endpoint during the test. Or as the response of the endpoint.
import { BASE_AUTH_TEST_BODY } from '../../../../CoreHelpers/testBody'
export const failBody = {
...BASE_AUTH_TEST_BODY,
// Add in any incorrect data to test the endpoints failure
}
export const failResponse = {
success: false,
error: 'Fatal error in Users_CreateUser_1_0_0. Investigate.',
message: 'No Body',
}
successData.ts
import { BASE_NO_AUTH_TEST_BODY } from '../../../../CoreHelpers/testBody' // Note this is different depending on if AUTH is required
import { Body, SuccessResponse } from '../types'
export const successBody: Body = {
...BASE_NO_AUTH_TEST_BODY,
// add in any data to the body to make the function work.
}
export const successResponse: SuccessResponse = {
success: true,
}
DataLayer
There is a main folder called DataLayer
This folder is for all the data calls. Like Prisma, SQL, NoSQL, File Storage, etc.
These are reusable files that can be used in every endpoint.
Functions in these files should be written like this:
export const functionName = async (
prisma: PrismaDB,
userId: string,
) => {
try {
return prisma.user.findFirst({
where: {
id: userId,
},
// ... etc..
})
} catch (error) {
console.error('functionName ERROR: ', error)
throw new Error('Error functionName')
}
}
OR like this:
export const upsertUser = async (
prisma: PrismaDB,
{
userId,
name,
phone,
}: {
userId?: string
name: string
phone: number
},
) => {
try {
return prisma.users.upsert({
where: { id: userId || '' },
update: {
userId,
name,
phone,
},
create: {
userId,
name,
phone,
},
select: {
id: true,
},
})
} catch (error) {
standardDataLayerErrorLog('upsertUser', error)
throw new Error('Error Getting upsertUser')
}
}
Authentication
Authentication is done out of the box.
If you want a user to be able to log in, or authenticate into your service. Just update the Core_Login_[version]
function.
It will always use the highest version of the Login Endpoint to authenticate.
Inside the Core_Login_1.0.0
endpoint you can add whatever logic you want to verify a user. Whether thats OAuth or Email login, or whatever your service needs.
The object that is returned from this endpoint will be encrypted, timestamped, your encryption key will be salted into it, and will be added to the login response as an auth
token.
The auth
token is required to be sent with every 'loginRequired' endpoint.
When a new endpoint is created, the question is asked: Does this function require a user to have authenticated JWT to access it?
If the answer to this is yes. Then the auth
token is required to be sent.
This auth token will be decrypted by wappi and returned into the endpoint body in the object:
{
...body,
auth: // Decrypted auth token from Core_Login_1.0.0 response
}
This means anything added to the Core_Login_1.0.0
endpoint will be available in every authenticated endpoint.
Which means you don't need to send that information through in the body as well.
Webhooks
Sometimes your service or app will require you to receive a webhook. This is usually sent by another service so you can't control the body or auth, etc. You get what you get and don't get upset.
Therefor Webhooks are handled slightly differently.
Your function must be in a nameSpace called Webhook
!!
The URL matters ==> A webhook must point at /Webhook_FunctionName_V_V_V
Similarly, all endpoints for webhooks must be namespaced in API/Webhook
The entire body of the payload will get passed through.
WARNING:: It is up to you to manually verify the authenticity of the payload.
Setting up your .env file
Every wappi repo should have a env.example
This file should be committed with no data in it.
So with the CI/CD process, it uses the .env.example to generate the env file.
To run on your CI/CD or locally use: wappi envfile
or wappi e
.
This command will generate a .env
file based from the .env.example
and will get any environment variables that are available and populate the .env
file with those values.
So locally, nothing will happen. But in a CI/CD pipeline, like GitLab. If you have variables set in your repo, they will be populated automatically. Pretty cool!
Lint
An eslint file comes with wappi.
To lint your project based on the rules in the .eslintrc.json
file, run wappi lint
or wappi l
Best practice: If everyone has eslint installed, they rules are committed and the project always stays consistent.
Main index.ts file
There is a main API/index.ts file.
This file has a very strict format and should not be changed by you, unless you know what you are doing!
If this file is incorrect, then the wappi server won't work.
When running wappi c
it will automatically add the endpoints to this file.
Running wappi d
will delete the endpoint you choose from this file automatically.
YOU SHOULD NOT TOUCH THIS FILE.
If you make an internal only endpoint, it will not be in this file.
// DO NOT REMOVE ANY COMMENTS!
exports.Endpoints = {
Core_TestNoAuthentication_1_0_0: true, // DO NOT DELETE THIS METHOD
Core_TestAuth_1_0_0: true, // DO NOT DELETE THIS METHOD
Core_Login_1_0_0: true, // DO NOT DELETE THIS METHOD
NewEndpoints_AddedHere_1_0_0: true,
// NewEndpointGoesHere
}
exports.LoginRequired = {
Core_TestAuth_1_0_0: true, // DO NOT DELETE THIS METHOD
Core_Login_1_0_0: true, // DO NOT DELETE THIS METHOD
NewAuthEndpoints_AddedHere_1_0_0: true,
// NewLoginRequiredGoesHere
}
How to test if your service is running? Health Checks!
When you have this running on a server, you may want to do a health check to make sure it's all happy and working.
To do a health check, simply hit the URL: /healthcheck
All http methods will return a 200 response.
Try it locally after running wappi run
> then go to the url: http://localhost:3030/healthcheck
Just hit {DOMAIN_URL}/healthcheck
If it's working, this will return:
{
"complete": true,
"statusCode": 200,
"message": "Healthy",
"success": true,
"devMessages": [] // This depends what environment you are in.
}
Response dev messages
in your .env
file, you can set the ENVIRONMENT
ENVIRONMENT - Either: 'localhost' | 'dev' | 'stage' | 'prod' - Rebuild when you change this
Like this ENVIRONMENT='localhost'
If you are running on localhost
| dev
| stage
. You will see an additional key in the response called "devMessages": []
This is an array of developer only messages that tell you why things are not working within wappi.
They are wappi server specific and have nothing to do with your code within an endpoint.
Testing authentication
In a service you might need to test if someone is authenticated or not.
Wappi has an out of the box function called: Core_TestAuth_1_0_0
If the user has a authenticated auth token, it will return:
{
"complete": true,
"statusCode": 200,
"message": "No message",
"success": true,
"authenticated": true,
"results": {
"success": true,
"message": "If you see this, the authentication is working.",
"auth": {}, // A decrypted response of the auth token (The response from Core_Login_1.0.0)
"environment": "localhost"
},
"devMessages": []
}
OR
{
"complete": true,
"statusCode": 200,
"message": "No message",
"success": true,
"authenticated": false,
"devMessages": [
"Your auth key is not authorized. Reissue it and try again.",
"Your access is denied. Login again if the function required authentication, or change the required auth to false."
]
}
Testing
Testing everything
To run all the index.spec.ts
files run wappi t
To test 1 endpoint
You can test 1 specific endpoint very easily by just running the following.
wappi t NameSpace_FunctionName_1_0_0
This will run the index.spec.ts
file for the endpoint name passed in.
Running Locally
To run the wappi server locally, just run:
wappi run
or wappi r
This will build all the endpoints and run the server on the port in your config (wappi.json) file.
This will NOT install your npm packages. So you need to run npm install
or yarn
before running it.
Running wappi will also send a compiled wappiTypes.ts
file to the paths in your .env
file.
Example:
FRONTEND_PROJECT_PATHS=["/Users/JamesBond/Project007/CasinoRoyale/src/"]
Types that will be excluded from the wappiTypes.ts file are:
EndPointBody
, SuccessResponse
, ErrorResponse
, Response
, BaseNoAuthBody
, CreateLogProps
, DB
, PrismaDB
, WappiBaseNoAuthBody
, WappiBaseAuthenticatedBody
, Body
This means that these are restricted to use. Or if they are used, they will not propagate through to your frontend wappiTypes.ts
file.
Using types on the front end
Many projects have types on the backend that need to be propagated through to the front end project.
With wappi you can write your types in the types.ts
file in the endpoint and it will be compiled into a single types file that will be automatically sent to your frontend project with the following set up in your .env
file.
Example:
FRONTEND_PROJECT_PATHS=["/Users/JamesBond/Project007/CasinoRoyale/src/"] # The local path you want it to go.
The end result will be 1 file with every type from the backend that is used in:
- Endpoint Body
- Endpoint SuccessResponse
- Endpoint ErrorResponse Here is an example:
type FunPersonOptions = 'fun' | 'notFun' | 'boring'
export interface NameSpace_FunctionName_1_0_0__Body {
userId: number
userName: string
funPerson: FunPersonOptions
}
export interface NameSpace_FunctionName_1_0_0__SuccessResponse {
success: true
matchName: string
toSexyForMyShirt: boolean
}
export interface NameSpace_FunctionName_1_0_0__ErrorResponse {
success: false
error: string
message: string
}
How to call it from the front end?
Always use the POST
Method
The URL doesn't matter. You can send this to domain.com/
or domain.com/ThisIsAMadeUpURL
It might help with logging if you have a url, but it's not needed. Except for a webhook. See webhooks.
How the type is structured within the query
{
name: "camelCaseName",
env: "PROD",
version: "1.0.0",
auth: "JWT_String",
query: "PascalCase_With_Underscores_For_NameSpacing",
data: {
...
},
results: { // Default is "result"
name: "camelCaseNameForResults",
include: [
{
name: "camelCaseName",
key: "keyNameToInclude"
}
],
exclude: [
{
key: "keyNameToExclude"
}
]
},
}
Query Structure
name : optional
This is the specific name of the information you want returned. As multiple requests can be made, this breaks them up.
version : required
The code version you want.
Options: 1.0.0
auth : optionally required - if necessary
The auth token of the user, which identifies they are authenticated.
query : required
This is the name of the function on the server.
This function will be the one called.
data : optional
This is an object of the all the data required for the function.
Such as all the user data to add a user.
This data is specific to the function and should be typed in the NameSpace_FunctionName_1_0_0__Body
type.
So look in the types.ts
file of the function your calling to see whats required.
results : optional
This determines what results you receive.
If you leave it as a blank obj or don't include it, it will return everything the function returns.
Otherwise you can specify what to include or exclude.
name : optional The name you want the results to return as. Default = results
include : optional An array of objects, with
name : required This is the name that you want it to be called. So you can rename it to match your frontend app.
key : required This is the backend key of the object that you want to include
exclude : optional An array of objects, with
key : required This is the backend key of the object that you want to exclude from the results. This allows the returned object to be clean to what you need, to help with clarity.
Logic for including and excluding is as follows:
If nothing is defined in include, then everything will be included.
If nothing is defined in exclude, then nothing will be excluded.
If Nothing is defined in include, but something is definded in exclude, everything defined in exclude will be removed.
If a key is defined in include and in exclude, then it will be included
Why do this?
Usually the processing of the endpoint is faster than the Latency of sending and receiving the data. So you might have an endpoint that can return a lot of data, but you only want 1 piece of it. So instead of creating a new function, you can just define what you want returned and get the same result.
wappi_frontend
There is a package called wappi_frontend
which is built for use on the frontend to call your backend wappi server.
Which has all the types in that and uses native js fetch. So no other packages required.
So... Just use that..
Prisma
We recommend using prisma, as it's awesome... So any database requests. Use Prisma ORM by default.
Updating wappi
When a new version of wappi is available, it will tell you in the terminal.
To update just run wappi update
or wappi u
This will switch you to the latest version of wappi.
If it's a major version, there might be some rework of your code to get to the latest practices.
Updating npm packages
This is a little risky to run, so be careful.
If you want to update ALL your node modules to teh latest versions run wappi update
or wappi u
This will prompt you to confirm you want to update ALL your packages..
And then it will update everything in your package.json
Building wappi
To build run wappi build
or wappi b
You would use this in your CI/CD pipeline.
Locally, you can use it, but you don't need to.
Running wappi run
or wappi r
will build it.
CI/CD Pipelines
All wappi commands also allow helper flags.
Currently ciPipeline
is the only one.
This can be used to run a light version of wappi
wappi help
Either use the wappi GPT that is publicly available here: https://chat.openai.com/g/g-wIGNHod1U-wappi-gpt
or to quickly find the wappi commands, run wappi help
or wappi h