
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.
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-identityreturns your account, you're set.
- The AWS SAM CLI
- Node.js 22 and npm, since
sam builduses npm to package the function
-
curlor 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.1Host: abc123.execute-api.us-east-1.amazonaws.comAccept: 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 OKcontent-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-31Description: A small REST-style API on AWS - the dad joke serviceGlobals:Function:Runtime: nodejs22.xTimeout: 5MemorySize: 128Resources:JokesApi:Type: AWS::Serverless::HttpApiProperties:StageName: $defaultGetJokeFunction:Type: AWS::Serverless::FunctionProperties:CodeUri: src/Handler: index.handlerEvents:GetJoke:Type: HttpApiProperties:ApiId: !Ref JokesApiPath: /jokes/{id}Method: GETOutputs:ApiUrl:Description: Invoke URL for the dad joke APIValue: !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 buildsam 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 404content-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
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article