AWS Builder Center
What Is a REST API? Build a Dad Joke API on AWS in 5 Minutes"

What Is a REST API? Build a Dad Joke API on AWS in 5 Minutes"

Learn what a REST API is (resources, HTTP methods, status codes, and stateless requests), then build and deploy a dad joke API with Amazon API Gateway, AWS Lambda, and AWS SAM.

Senior Developer Advocate
I built a REST API that tells dad jokes. Let me show you.
Bash
curl "$API_URL/jokes/7"
JSON
{"id":"7","setup":"Why was the Lambda function so tired?","punchline":"Too many cold starts.","groan":3}
Thanks, I'll be here all week. 😆
Now look at what actually happened. I sent a GET request asking for joke number 7, and the API sent it back as JSON. That's a REST API in its simplest form.
In this post I'll explain what a REST API is, then build that exact dad joke API on AWS with  Amazon API Gateway ,  AWS Lambda , and AWS SAM. By the end you'll have your own deployed endpoint to fill with your own jokes. Full disclosure, I'm a Developer Advocate at AWS, so these are the tools I reach for.
Let's jump in!




Prerequisites

The first half of this post is concepts only, so you don't need anything installed to follow along. For the build, you'll want:
  • An AWS account
  • The  AWS CLI  with credentials configured. If  aws sts get-caller-identity  returns your account, you're set.
  • Node.js 22 and npm, since  sam build  uses npm to package the function
  •  curl  or any other HTTP client

What is an API?

API stands for application programming interface. You can also just think of it as a contract. You send a request in a shape the service accepts, and you get back a response in the shape you expected.

So what does that actually look like? Here's the raw HTTP request behind that joke 7 call:
http
GET /jokes/7 HTTP/1.1
Host: abc123.execute-api.us-east-1.amazonaws.com
Accept: application/json

A request has four parts. The method ( GET ) is what you want to do. The path ( /jokes/7 ) is where to find it. The headers carry details about the request, like what format you'd like back or an auth token. And there's an optional body, which is the data you're sending along. A GET doesn't need one, so it's missing here.

The response that came back looks like this:
http
HTTP/1.1 200 OK
content-type: application/json

{"id":"7","setup":"Why was the Lambda function so tired?","punchline":"Too many cold starts.","groan":3}

Responses have three parts: a status code ( 200 OK ), headers, and usually a body. Once you can read these two blocks, every API you work with gets a lot less mysterious.

What makes an API RESTful?

REST stands for Representational State Transfer. It's an architectural style for how software talks to other software over a network, and it's almost always used over HTTP. That name sounds academic, so here's what it means in practice.

Resources are the nouns

A REST API is organized around resources. You can imagine things like users or books. Mine is dad jokes, because that's more interesting than books and I'm a dad.
Each resource gets its own address.  /jokes  is the collection of all jokes, and  /jokes/7  is one specific joke. The thing you get back is a representation of that resource, usually JSON.

HTTP methods are the verbs

You'll often hear the basic operations called CRUD, which stands for create, read, update, and delete. Those map onto HTTP methods:
Operation
Request
What it does
Read
 GET /jokes/7 
Pulls up a joke
Create
 POST /jokes 
Submits a new joke
Update
 PUT /jokes/7 
Replaces the whole joke
Delete
 DELETE /jokes/7 
Takes the joke down
There are more methods, like  PATCH  for partial updates, but these four cover most of what you'll do.

Status codes tell you what happened

Every response comes back with a status code. 200 means OK, it worked. 404 means the resource isn't there. 400 means the request you sent was probably wrong, and 500 means something broke on the server.

The first digit tells you who to blame. A 4xx code points at the client, and a 5xx code points at the server. So if you get a 4xx back, check your request before you go digging through server logs.

Every request stands on its own

REST is stateless. Every request carries everything the server needs to handle it, and the server forgets the client the moment it sends a reply. If you need to be logged in, your token rides along on every single request.

That's why Lambda is a good fit here. Lambda is built so you don't count on anything sticking around between calls, which is basically the same idea. (Lambda can reuse a warm execution environment, so some things in memory may survive between invocations, but you can't rely on it.)

People use the word REST loosely

Strictly speaking, REST has a set of architectural constraints, including a uniform interface, that a lot of so-called REST APIs don't fully follow.  MDN's REST glossary entry  calls this out too. Most APIs that get called REST are JSON over HTTP that follow the core ideas above.

That's what we're building. It's a basic interface, and I'm not going to get into the stricter parts of the architecture in this post.

How the AWS pieces fit together

For the dad joke API, two services do the work:
text
Client → Amazon API Gateway → AWS Lambda → Amazon API Gateway → Client

API Gateway is the public HTTPS entry point. It owns the URL and routes each request to the right place. Lambda is where the code runs after it's called. The Lambda function hands back a status code and a body, then API Gateway turns that into an HTTP response and returns it to the client.
To keep it simple, I hard-coded the jokes right into the Lambda function. A real API would store them in a database like Amazon DynamoDB, and I'll point you at a tutorial for that at the end.

HTTP API or REST API? Picking an API Gateway type

This part trips people up, so I'm giving it its own section.

API Gateway offers a few API types. One of them is literally named REST API. Another is named HTTP API. AWS describes both as RESTful API products, so either one can serve the API we're building. The names tell you which API Gateway features you get. They don't tell you whether your API follows REST.

HTTP API is the simpler and cheaper option, and it's what you want for a Lambda-backed endpoint like this one. You pick the REST API product when you need a fuller API management feature set:
  • API keys and usage plans
  • Per-client throttling
  • Request validation
  • Caching
  • AWS WAF integration
  • Private endpoints

I'm not using any of that, so I stuck with HTTP API. AWS keeps a full comparison in  Choose between REST APIs and HTTP APIs  if you want to check a specific feature.

Step 1: Create the project

You could click through the AWS console to set all this up by hand, which some people call ClickOps. I'd rather use  AWS SAM , so the infrastructure lives in code I can rerun and delete later.
The whole project is three files:

text
jokes-api/
├── template.yaml
└── src/
├── index.mjs
└── package.json

 template.yaml  describes the AWS resources,  index.mjs  is the Lambda handler, and  package.json  is a one-liner:
JSON
{
"type": "module"
}

 sam build  looks for a  package.json  in the function folder, and it's where dependencies would go if you added any. This API doesn't have any.

Step 2: Write the SAM template

Real quick, because I know what you're thinking. Nobody types this from memory. Running  sam init  scaffolds a working template you can edit.

And if you use an AI coding agent like Kiro, the  Agent Toolkit for AWS  is free and well worth installing. It points your agent at current AWS docs instead of whatever it memorized in its training data. That matters for templates like this one, where runtime versions change every year.
Here's the full  template.yaml :
YAML
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: A small REST-style API on AWS - the dad joke service

Globals:
Function:
Runtime: nodejs22.x
Timeout: 5
MemorySize: 128

Resources:
JokesApi:
Type: AWS::Serverless::HttpApi
Properties:
StageName: $default

GetJokeFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: index.handler
Events:
GetJoke:
Type: HttpApi
Properties:
ApiId: !Ref JokesApi
Path: /jokes/{id}
Method: GET

Outputs:
ApiUrl:
Description: Invoke URL for the dad joke API
Value: !Sub "https://${JokesApi}.execute-api.${AWS::Region}.amazonaws.com"

The  Transform  line at the top is what makes this AWS SAM instead of raw AWS CloudFormation. SAM expands it at deploy time, so I get to describe a Lambda function and an API in about 30 lines instead of a few hundred.

There are two resources.  JokesApi  is the HTTP API (that's the  AWS::Serverless::HttpApi  type from the last section), and  GetJokeFunction  is the Lambda function.
The  Events  block does two jobs at once. It creates the route  GET /jokes/{id}  on the API, and it wires that route to this function. The  {id}  in curly braces is a path parameter, so one route serves every joke I have. Whatever you put in that slot of the URL gets passed to the function.
The  ApiUrl  output prints the invoke URL after the deploy, so you don't have to go find it in the console.

Step 3: Write the Lambda handler

Here's  src/index.mjs :
js
const jokes = {
"1": {
id: "1",
setup: "Why do REST APIs never get invited to parties?",
punchline: "They can't hold any state.",
groan: 4
},
"7": {
id: "7",
setup: "Why was the Lambda function so tired?",
punchline: "Too many cold starts.",
groan: 3
}
};

export const handler = async (event) => {
const { id } = event.pathParameters;
const joke = jokes[id];

if (!joke) {
return {
statusCode: 404,
headers: { "content-type": "application/json" },
body: JSON.stringify({ error: `No joke with id ${id}` })
};
}

return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify(joke)
};
};
API Gateway hands Lambda an event object, and the path parameter is sitting right on it as  event.pathParameters.id . That's the  {id}  from the template.

I set up two outcomes. If there's a joke at that id, the function returns a 200 with the joke as JSON. If not, it returns a 404 with an error message. Each return value has a status code, headers, and a body, which are the same three response parts from earlier. API Gateway turns that object into the actual HTTP response.

This is all stateless, too. The  jokes  object never changes, and nothing in the handler holds on to anything between calls.

Step 4: Build and deploy

From the  jokes-api  folder, build and deploy:
Bash
sam build
sam deploy --guided

 --guided  walks you through a few prompts the first time. Pick a stack name like  jokes-api  and a region, and accept the defaults for the rest. One prompt will warn you that  GetJokeFunction  has no authentication defined and ask if that's okay. For this demo, answer  y . I'll come back to why that matters before you ship anything real.

SAM saves your answers to  samconfig.toml , so next time a plain  sam deploy  is enough.
The deploy creates the API Gateway API and its route, the Lambda function, and the IAM role that lets API Gateway invoke the function. It takes a minute or two. When it finishes, look for the  ApiUrl  value in the outputs and save it to a variable:
Bash
export API_URL="https://YOUR_API_ID.execute-api.YOUR_REGION.amazonaws.com"

Step 5: Call the API

Moment of truth:
Bash
curl "$API_URL/jokes/7"
JSON
{"id":"7","setup":"Why was the Lambda function so tired?","punchline":"Too many cold starts.","groan":3}

There it is. The joke came back as JSON with a 200, just like we expected. Let's try a different one:
Bash
curl "$API_URL/jokes/1"
JSON
{"id":"1","setup":"Why do REST APIs never get invited to parties?","punchline":"They can't hold any state.","groan":4}

Same route, different id, different joke. That's the path parameter doing its job.
Now ask for a joke that doesn't exist. Add  -i  so curl shows the status line and headers:
Bash
curl -i "$API_URL/jokes/9999"
http
HTTP/2 404
content-type: application/json

{"error":"No joke with id 9999"}

You'll see a few more headers in your terminal than I've shown here. The important part is the 404 and the error body. It's a different status code and a different body, but the same route and the same function handled it.
That's a REST API on AWS. 🎉

Before you put an API like this in production

Keep in mind this is just one part of the puzzle. I skipped several things on purpose to keep the demo small, and you'll want them before anyone depends on your API.

Authentication comes first. Unless you actually want anyone on the internet calling it, add an authorizer. HTTP APIs support  JWT authorizers, Lambda authorizers, and IAM authorization .
You'll also want to store the data somewhere real instead of in the handler, and set up CloudWatch alarms so you hear about errors or traffic spikes before your bill does. API Gateway can help you  throttle requests , but you'll need to configure limits that make sense for your traffic. And if you need things like request validation or per-client usage plans, that's when the REST API product from earlier starts to make sense.

Cleanup

At zero traffic this stack costs basically nothing, but take it down anyway when you're done so you're not leaving resources sitting in your account:

Bash
sam delete

Confirm the prompts, and SAM deletes the CloudFormation stack along with the API, the function, and the IAM role.

Wrapping up

REST is an architectural style for organizing how software talks to other software over a network, almost always over HTTP. It's built on resources, HTTP verbs, status codes, and stateless requests. On AWS, API Gateway routes the request and Lambda runs your code.

The part I most want you to remember is the naming. HTTP API and REST API are both just API Gateway products, and either one can serve a RESTful API. Pick based on the features you need, not on which one has "REST" in the name. For a small Lambda-backed API like this, I'd start with HTTP API every time.

If you want a next step, take the code and add DynamoDB so the jokes persist and you can  POST  your own. The  Lambda and API Gateway CRUD tutorial  is a good place to start.
Let me know in the comments what you'd like to see next, or drop your best dad joke. Until next time.

Resources:
  • Source code: REPLACE_WITH_REPOSITORY_URL
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article